{"spec_id":"scatter-annotated","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-annotated: Annotated Scatter Plot with Text Labels\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 90, bottom: 100, left: 120 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Deterministic PRNG (tiny LCG — Math.random() is not reproducible) -----\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\n// --- Data: R&D spend vs. annual revenue for fictional tech companies -------\nconst companies = [\n  \"Nimbus Labs\", \"Cascade Systems\", \"Vertex Dynamics\", \"Solstice Tech\",\n  \"Ironwood Analytics\", \"Beacon Robotics\", \"Quartz Networks\", \"Halcyon AI\",\n  \"Meridian Software\", \"Driftwood Data\", \"Lumen Photonics\", \"Argent Cloud\",\n  \"Tidewater Semiconductors\", \"Cobalt Interactive\", \"Palisade Security\",\n  \"Everline Biotech\", \"Fernwood Materials\", \"Aurora Aerospace\",\n];\n\nconst data = companies.map((label) => {\n  const rdSpend = 18 + rand() * 380;\n  const revenue = Math.max(35, rdSpend * (2.6 + rand() * 4.4) + (rand() - 0.5) * 300);\n  return { label, x: rdSpend, y: revenue };\n});\n\n// --- Scales -------------------------------------------------------------\nconst x = d3.scaleLinear().domain(d3.extent(data, (d) => d.x)).nice().range([0, iw]);\nconst y = d3.scaleLinear().domain([0, d3.max(data, (d) => d.y)]).nice().range([ih, 0]);\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Gridlines (both axes, per scatter convention) -------------------------\ng.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).tickSize(-ih).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\ng.append(\"g\")\n  .call(d3.axisLeft(y).ticks(7).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(8));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(7));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\").style(\"font-family\", \"sans-serif\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").remove();\n}\n\n// --- Axis labels --------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 30)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-family\", \"sans-serif\")\n  .text(\"R&D Spending ($M)\");\n\nsvg.append(\"text\")\n  .attr(\"transform\", `translate(${36},${margin.top + ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-family\", \"sans-serif\")\n  .text(\"Annual Revenue ($M)\");\n\n// --- Marker + label layout --------------------------------------------------\nconst markerRadius = 10;\n\n// --- Trend-relative highlight: which company earns the most revenue per\n// R&D dollar (the story's focal point), and which points are worth naming --\nconst n = data.length;\nconst sumX = d3.sum(data, (d) => d.x);\nconst sumY = d3.sum(data, (d) => d.y);\nconst sumXY = d3.sum(data, (d) => d.x * d.y);\nconst sumXX = d3.sum(data, (d) => d.x * d.x);\nconst slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);\nconst intercept = (sumY - slope * sumX) / n;\nconst withResidual = data.map((d) => ({ ...d, residual: d.y - (slope * d.x + intercept) }));\nconst standout = withResidual.reduce((a, b) => (b.residual > a.residual ? b : a));\n\n// Label only the notable subset — biggest over/under-performers relative to\n// the trend, plus the R&D and revenue extremes — per the spec's guidance to\n// annotate a subset rather than every point once the dataset gets dense.\nconst maxX = d3.max(data, (d) => d.x);\nconst maxY = d3.max(data, (d) => d.y);\nconst keyLabels = new Set(\n  [...withResidual]\n    .sort((a, b) => Math.abs(b.residual) - Math.abs(a.residual))\n    .slice(0, 8)\n    .map((d) => d.label)\n);\nkeyLabels.add(standout.label);\nfor (const d of data) if (d.x === maxX || d.y === maxY) keyLabels.add(d.label);\n\n// The trend-beating standout renders larger and at full opacity to act as a\n// focal point; the rest stay uniform to keep the density-driven alpha~0.7.\nconst points = data.map((d) => ({\n  ...d,\n  px: x(d.x),\n  py: y(d.y),\n  r: d.label === standout.label ? markerRadius + 5 : markerRadius,\n}));\n\n// --- Points ---------------------------------------------------------------\ng.selectAll(\".point\")\n  .data(points)\n  .join(\"circle\")\n  .attr(\"class\", \"point\")\n  .attr(\"cx\", (d) => d.px)\n  .attr(\"cy\", (d) => d.py)\n  .attr(\"r\", (d) => d.r)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", (d) => (d.label === standout.label ? 1 : 0.7))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", (d) => (d.label === standout.label ? 2.5 : 1.5));\n\n// --- Greedy label placement (a hand-rolled adjustText equivalent) ---------\n// D3 has no adjustText port, but the render harness runs in a real browser,\n// so text width is measured with actual SVG layout (getBBox) rather than\n// estimated — jsdom can't do this, a headless Chromium can.\nconst labeledPoints = points.filter((d) => keyLabels.has(d.label));\nconst measure = svg.append(\"text\").attr(\"opacity\", 0).style(\"font-size\", \"13px\").style(\"font-family\", \"sans-serif\");\nconst labelWidth = new Map(labeledPoints.map((d) => {\n  measure.text(d.label);\n  return [d.label, measure.node().getBBox().width];\n}));\nmeasure.remove();\nconst labelHeight = 15;\n\nconst compass = [\n  { dx: 1, dy: -1 }, { dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: -1, dy: -1 },\n  { dx: 1, dy: 1 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: -1, dy: 1 },\n];\nconst radii = [28, 42, 58, 76, 96];\nconst pad = 5;\n\n// The chosen box must stay within the canvas (g is translated by margin.left/\n// margin.top, so the usable local-coordinate range extends margin.right/bottom\n// past iw/ih but not past the page edge) — otherwise a label near a domain\n// extreme can be pushed off the saved PNG (AR-09 edge clipping).\nconst edgePad = 4;\nconst boundsMinX = -margin.left + edgePad;\nconst boundsMaxX = iw + margin.right - edgePad;\nconst boundsMinY = -margin.top + edgePad;\nconst boundsMaxY = ih + margin.bottom - edgePad;\nfunction inCanvasBounds(box) {\n  return box.x0 >= boundsMinX && box.x1 <= boundsMaxX && box.y0 >= boundsMinY && box.y1 <= boundsMaxY;\n}\n\nfunction labelBox(px, py, dir, r, w, h) {\n  const anchorX = px + dir.dx * r;\n  const anchorY = py + dir.dy * r;\n  const x0 = dir.dx > 0 ? anchorX : dir.dx < 0 ? anchorX - w : anchorX - w / 2;\n  const y0 = dir.dy > 0 ? anchorY - h * 0.2 : dir.dy < 0 ? anchorY - h * 0.9 : anchorY - h / 2;\n  return { x0, y0, x1: x0 + w, y1: y0 + h, anchorX, anchorY };\n}\n\nfunction overlaps(a, b) {\n  return a.x0 < b.x1 + pad && a.x1 + pad > b.x0 && a.y0 < b.y1 + pad && a.y1 + pad > b.y0;\n}\n\nconst markerBoxes = points.map((d) => ({\n  label: d.label,\n  x0: d.px - d.r - pad, y0: d.py - d.r - pad,\n  x1: d.px + d.r + pad, y1: d.py + d.r + pad,\n}));\n\nconst placedBoxes = [];\nconst placed = labeledPoints.map((d) => {\n  const w = labelWidth.get(d.label);\n  let chosen = null;\n  outer: for (const r of radii) {\n    for (const dir of compass) {\n      const box = labelBox(d.px, d.py, dir, r, w, labelHeight);\n      if (!inCanvasBounds(box)) continue;\n      const hitsMarker = markerBoxes.some((m) => m.label !== d.label && overlaps(box, m));\n      const hitsLabel = placedBoxes.some((p) => overlaps(box, p));\n      if (!hitsMarker && !hitsLabel) {\n        chosen = { box, dir, r };\n        break outer;\n      }\n    }\n  }\n  // Relaxed pass: allow overlap but never allow the label to run off-canvas —\n  // a collision is a legibility nit, an edge clip is an auto-reject.\n  if (!chosen) {\n    outerRelaxed: for (const r of radii) {\n      for (const dir of compass) {\n        const box = labelBox(d.px, d.py, dir, r, w, labelHeight);\n        if (inCanvasBounds(box)) {\n          chosen = { box, dir, r };\n          break outerRelaxed;\n        }\n      }\n    }\n  }\n  if (!chosen) {\n    const dir = compass[0];\n    const r = radii[radii.length - 1];\n    let box = labelBox(d.px, d.py, dir, r, w, labelHeight);\n    let shiftX = 0;\n    if (box.x1 > boundsMaxX) shiftX = boundsMaxX - box.x1;\n    else if (box.x0 < boundsMinX) shiftX = boundsMinX - box.x0;\n    let shiftY = 0;\n    if (box.y1 > boundsMaxY) shiftY = boundsMaxY - box.y1;\n    else if (box.y0 < boundsMinY) shiftY = boundsMinY - box.y0;\n    box = {\n      x0: box.x0 + shiftX, x1: box.x1 + shiftX,\n      y0: box.y0 + shiftY, y1: box.y1 + shiftY,\n      anchorX: box.anchorX + shiftX, anchorY: box.anchorY + shiftY,\n    };\n    chosen = { box, dir, r };\n  }\n  placedBoxes.push(chosen.box);\n  return {\n    ...d,\n    dir: chosen.dir,\n    anchorX: chosen.box.anchorX,\n    anchorY: chosen.box.anchorY,\n    textAnchor: chosen.dir.dx > 0 ? \"start\" : chosen.dir.dx < 0 ? \"end\" : \"middle\",\n    baseline: chosen.dir.dy > 0 ? \"hanging\" : chosen.dir.dy < 0 ? \"auto\" : \"middle\",\n  };\n});\n\n// --- Leader lines (subtle, connecting offset labels back to their point) ---\ng.selectAll(\".leader\")\n  .data(placed)\n  .join(\"line\")\n  .attr(\"class\", \"leader\")\n  .attr(\"x1\", (d) => d.px + d.dir.dx * (d.r + 3))\n  .attr(\"y1\", (d) => d.py + d.dir.dy * (d.r + 3))\n  .attr(\"x2\", (d) => d.anchorX - d.dir.dx * 5)\n  .attr(\"y2\", (d) => d.anchorY - d.dir.dy * 5)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1)\n  .attr(\"opacity\", 0.5);\n\n// --- Labels -------------------------------------------------------------\ng.selectAll(\".label\")\n  .data(placed)\n  .join(\"text\")\n  .attr(\"class\", \"label\")\n  .attr(\"x\", (d) => d.anchorX)\n  .attr(\"y\", (d) => d.anchorY)\n  .attr(\"text-anchor\", (d) => d.textAnchor)\n  .attr(\"dominant-baseline\", (d) => d.baseline)\n  .attr(\"fill\", (d) => (d.label === standout.label ? t.ink : t.inkSoft))\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", (d) => (d.label === standout.label ? \"600\" : \"400\"))\n  .style(\"font-family\", \"sans-serif\")\n  .text((d) => d.label);\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\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .style(\"font-family\", \"sans-serif\")\n  .text(\"scatter-annotated · javascript · d3 · anyplot.ai\");\n"}