{"spec_id":"scatter-text","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-text: Scatter Plot with Text Labels Instead of Points\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 120, right: 50, bottom: 50, left: 50 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic LCG — the browser has no seeded RNG) ---\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n}\n\n// Programming languages positioned as if by dimensionality reduction over a\n// paradigm/use-case embedding — four loosely separated islands, matching the\n// \"word embeddings after t-SNE/UMAP\" application from the specification.\nconst CATEGORIES = [\n  { name: \"Systems\", cx: 25, cy: 75 },\n  { name: \"Web\", cx: 75, cy: 75 },\n  { name: \"Data Science\", cx: 25, cy: 25 },\n  { name: \"Functional\", cx: 75, cy: 25 },\n];\n\n// [label, prominence] — prominence (1-3) drives font size + opacity so\n// well-known languages read first, echoing real embedding-plot hierarchy.\nconst LANGUAGES = {\n  Systems: [[\"C\", 3], [\"C++\", 3], [\"Rust\", 3], [\"Go\", 3], [\"Fortran\", 2], [\"Zig\", 1], [\"Ada\", 1], [\"Assembly\", 1], [\"D\", 1], [\"Nim\", 1]],\n  Web: [[\"JavaScript\", 3], [\"TypeScript\", 3], [\"PHP\", 2], [\"Ruby\", 2], [\"HTML\", 2], [\"CSS\", 2], [\"Perl\", 1], [\"Dart\", 1], [\"Elm\", 1], [\"CoffeeScript\", 1]],\n  \"Data Science\": [[\"Python\", 3], [\"R\", 3], [\"SQL\", 2], [\"Julia\", 2], [\"MATLAB\", 2], [\"Scala\", 2], [\"SAS\", 1], [\"Stata\", 1], [\"Mathematica\", 1], [\"SPSS\", 1]],\n  Functional: [[\"Haskell\", 2], [\"Elixir\", 2], [\"Lisp\", 1], [\"Clojure\", 1], [\"Erlang\", 1], [\"F#\", 1], [\"OCaml\", 1], [\"Scheme\", 1], [\"Prolog\", 1], [\"Racket\", 1]],\n};\n\nconst raw = [];\nfor (const cat of CATEGORIES) {\n  for (const [label, prominence] of LANGUAGES[cat.name]) {\n    raw.push({\n      label,\n      category: cat.name,\n      prominence,\n      dataX: cat.cx + (rand() - 0.5) * 32,\n      dataY: cat.cy + (rand() - 0.5) * 32,\n    });\n  }\n}\n\nconst FONT_SIZE = { 1: 16, 2: 18, 3: 20 };\nconst OPACITY = { 1: 0.7, 2: 0.85, 3: 1 };\nconst halfWidth = (d) => (d.label.length * FONT_SIZE[d.prominence]) / 3.1 + 4;\n\n// --- Scales -------------------------------------------------------------\nconst x = d3.scaleLinear().domain(d3.extent(raw, (d) => d.dataX)).range([0, iw]);\nconst y = d3.scaleLinear().domain(d3.extent(raw, (d) => d.dataY)).range([ih, 0]);\nconst color = d3.scaleOrdinal().domain(CATEGORIES.map((c) => c.name)).range(t.palette);\n\n// Target pixel positions from the data coordinates, then let a stopped\n// force simulation nudge only the colliding labels apart (forceX/forceY pull\n// each label back toward its true coordinate; forceCollide keeps label boxes\n// from overlapping) — ticked synchronously since the render is a single frame.\nconst data = raw.map((d) => {\n  const px = x(d.dataX);\n  const py = y(d.dataY);\n  return { ...d, px, py, x: px, y: py };\n});\nconst simulation = d3\n  .forceSimulation(data)\n  .force(\"x\", d3.forceX((d) => d.px).strength(0.9))\n  .force(\"y\", d3.forceY((d) => d.py).strength(0.9))\n  .force(\"collide\", d3.forceCollide(halfWidth).iterations(4))\n  .stop();\nfor (let i = 0; i < 250; i += 1) simulation.tick();\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// --- Labels-as-markers ------------------------------------------------------\ng.selectAll(\"text.point\")\n  .data(data)\n  .join(\"text\")\n  .attr(\"class\", \"point\")\n  .attr(\"x\", (d) => d.x)\n  .attr(\"y\", (d) => d.y)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"dominant-baseline\", \"central\")\n  .style(\"font-size\", (d) => `${FONT_SIZE[d.prominence]}px`)\n  .style(\"font-weight\", (d) => (d.prominence === 3 ? 600 : 400))\n  .style(\"opacity\", (d) => OPACITY[d.prominence])\n  .attr(\"fill\", (d) => color(d.category))\n  .text((d) => d.label);\n\n// --- Legend (category color key, horizontally centered via getBBox) --------\nconst legend = svg.append(\"g\").attr(\"class\", \"legend\");\nconst items = legend.selectAll(\"g.item\").data(CATEGORIES).join(\"g\").attr(\"class\", \"item\");\n\nitems.append(\"circle\").attr(\"r\", 7).attr(\"fill\", (d) => color(d.name));\nitems\n  .append(\"text\")\n  .attr(\"x\", 16)\n  .attr(\"y\", 5)\n  .style(\"font-size\", \"15px\")\n  .attr(\"fill\", t.inkSoft)\n  .text((d) => d.name);\n\nconst gaps = [];\nitems.each(function () {\n  gaps.push(this.getBBox().width + 36);\n});\nconst totalWidth = d3.sum(gaps) - 36;\nlet cursor = (width - totalWidth) / 2;\nitems.each(function (d, i) {\n  d3.select(this).attr(\"transform\", `translate(${cursor},78)`);\n  cursor += gaps[i];\n});\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"scatter-text · javascript · d3 · anyplot.ai\");\n\n// --- Minimal dimension labels (this is an embedding plot: axes are latent\n// components, not measured units, so ticks would be misleading — just orient\n// the viewer with the two axis names) ---------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 14)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .style(\"letter-spacing\", \"0.04em\")\n  .text(\"Component 1\");\n\nsvg\n  .append(\"text\")\n  .attr(\"transform\", `translate(${16},${margin.top + ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .style(\"letter-spacing\", \"0.04em\")\n  .text(\"Component 2\");\n"}