{"spec_id":"arc-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// arc-basic: Basic Arc Diagram\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 100, right: 60, bottom: 90, left: 60 };\nconst iw = width - margin.left - margin.right;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Character interactions across chapters of a novel, in order of first appearance.\nconst characters = [\n  \"Elena\",\n  \"Marcus\",\n  \"Priya\",\n  \"Tobias\",\n  \"Wren\",\n  \"Osei\",\n  \"Ingrid\",\n  \"Ravi\",\n  \"Nadia\",\n  \"Colton\",\n  \"Beatrix\",\n  \"Sana\",\n];\n\nconst interactions = [\n  { source: \"Elena\", target: \"Marcus\", weight: 8 },\n  { source: \"Elena\", target: \"Priya\", weight: 5 },\n  { source: \"Marcus\", target: \"Tobias\", weight: 6 },\n  { source: \"Priya\", target: \"Tobias\", weight: 3 },\n  { source: \"Priya\", target: \"Wren\", weight: 7 },\n  { source: \"Tobias\", target: \"Osei\", weight: 4 },\n  { source: \"Wren\", target: \"Osei\", weight: 2 },\n  { source: \"Wren\", target: \"Ingrid\", weight: 6 },\n  { source: \"Osei\", target: \"Ravi\", weight: 3 },\n  { source: \"Ingrid\", target: \"Ravi\", weight: 5 },\n  { source: \"Ingrid\", target: \"Nadia\", weight: 4 },\n  { source: \"Ravi\", target: \"Colton\", weight: 2 },\n  { source: \"Nadia\", target: \"Colton\", weight: 6 },\n  { source: \"Nadia\", target: \"Beatrix\", weight: 3 },\n  { source: \"Colton\", target: \"Sana\", weight: 5 },\n  { source: \"Beatrix\", target: \"Sana\", weight: 4 },\n  { source: \"Elena\", target: \"Wren\", weight: 3 },\n  { source: \"Marcus\", target: \"Osei\", weight: 2 },\n  { source: \"Elena\", target: \"Sana\", weight: 2 },\n  { source: \"Priya\", target: \"Ingrid\", weight: 3 },\n];\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\nconst axisY = height - margin.bottom;\nconst x = d3.scalePoint().domain(characters).range([0, iw]).padding(0.5);\n\nconst maxWeight = d3.max(interactions, (d) => d.weight);\nconst strokeWidth = d3.scaleLinear().domain([1, maxWeight]).range([1.5, 7]);\nconst opacity = d3.scaleLinear().domain([1, maxWeight]).range([0.25, 0.65]);\n\n// Arc height scales with node distance (long-range connections arc higher).\n// scaleSqrt (not linear) so the single far-outlier span (Elena-Sana) doesn't\n// dominate the domain and compress the other 19 arcs into a thin band -\n// ordering is preserved (the long-range arc still reads as tallest) while the\n// short/medium arcs fill much more of the available canvas height.\nconst maxSpan = d3.max(interactions, (d) => Math.abs(x(d.source) - x(d.target)));\nconst arcHeight = d3.scaleSqrt().domain([0, maxSpan]).range([40, axisY - margin.top - 40]);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},0)`);\n\n// --- Arcs -----------------------------------------------------------------\ng.selectAll(\"path\")\n  .data(interactions)\n  .join(\"path\")\n  .attr(\"d\", (d) => {\n    const x1 = x(d.source);\n    const x2 = x(d.target);\n    const xm = (x1 + x2) / 2;\n    const h = arcHeight(Math.abs(x1 - x2));\n    return `M ${x1} ${axisY} Q ${xm} ${axisY - h * 2} ${x2} ${axisY}`;\n  })\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", (d) => strokeWidth(d.weight))\n  .attr(\"stroke-opacity\", (d) => opacity(d.weight))\n  .attr(\"stroke-linecap\", \"round\");\n\n// --- Baseline ---------------------------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", axisY)\n  .attr(\"y2\", axisY)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Nodes --------------------------------------------------------------------\ng.selectAll(\"circle\")\n  .data(characters)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d))\n  .attr(\"cy\", axisY)\n  .attr(\"r\", 8)\n  .attr(\"fill\", t.pageBg)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2);\n\n// --- Node labels ----------------------------------------------------------\ng.selectAll(\"text.node-label\")\n  .data(characters)\n  .join(\"text\")\n  .attr(\"class\", \"node-label\")\n  .attr(\"x\", (d) => x(d))\n  .attr(\"y\", axisY + 34)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d);\n\n// --- Title ------------------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"19px\") // scaled from 22px default: round(22 * 67/76), title is 76 chars\n  .style(\"font-weight\", \"600\")\n  .text(\"Character Interactions in a Novel · arc-basic · javascript · d3 · anyplot.ai\");\n"}