{"spec_id":"scatter-embedding","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-embedding: t-SNE and UMAP Embedding Visualization\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 93/100 | Created: 2026-08-11\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 130, right: 280, bottom: 70, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Deterministic RNG (LCG + Box-Muller) -----------------------------------\nlet seed = 42;\nconst rand = () => {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n};\nconst randNormal = () => {\n  const u1 = Math.max(rand(), 1e-6);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n};\n\n// --- Data: synthetic NLP document embeddings, projected via t-SNE ----------\n// Each topic cluster gets its own center, elongation, and rotation so the\n// blobs read like genuine t-SNE output rather than perfect circles.\n// Centers pushed further from the origin (vs. a tighter first draft) and the\n// Technology/Finance ellipses de-aligned + slimmed so their spreads no longer\n// bleed into one another near the top of the ring.\nconst clusterDefs = [\n  { name: \"Technology\", n: 120, cx: -9.1, cy: 7.7, sx: 1.3, sy: 0.9, angle: -0.15 },\n  { name: \"Sports\", n: 95, cx: 9.1, cy: 7.7, sx: 1.1, sy: 1.4, angle: -0.3 },\n  { name: \"Politics\", n: 110, cx: -9.8, cy: -6.3, sx: 1.3, sy: 1.1, angle: 0.9 },\n  { name: \"Health\", n: 85, cx: 7.7, cy: -7, sx: 1.0, sy: 1.6, angle: -0.6 },\n  { name: \"Finance\", n: 100, cx: 0, cy: 11.9, sx: 1.5, sy: 0.8, angle: 0.1 },\n  { name: \"Entertainment\", n: 90, cx: 0, cy: -11.9, sx: 1.5, sy: 0.9, angle: -0.2 },\n];\n\nconst points = [];\nfor (const c of clusterDefs) {\n  for (let i = 0; i < c.n; i++) {\n    const rx = randNormal() * c.sx;\n    const ry = randNormal() * c.sy;\n    const px = c.cx + rx * Math.cos(c.angle) - ry * Math.sin(c.angle);\n    const py = c.cy + rx * Math.sin(c.angle) + ry * Math.cos(c.angle);\n    points.push({ x: px, y: py, cluster: c.name });\n  }\n}\n\nconst clusters = clusterDefs.map((c) => {\n  const pts = points.filter((p) => p.cluster === c.name);\n  return {\n    name: c.name,\n    cx: d3.mean(pts, (p) => p.x),\n    cy: d3.mean(pts, (p) => p.y),\n  };\n});\n\n// --- Scales -------------------------------------------------------------\nconst xExtent = d3.extent(points, (p) => p.x);\nconst yExtent = d3.extent(points, (p) => p.y);\nconst xPad = (xExtent[1] - xExtent[0]) * 0.1;\nconst yPad = (yExtent[1] - yExtent[0]) * 0.1;\n\nconst x = d3.scaleLinear().domain([xExtent[0] - xPad, xExtent[1] + xPad]).range([0, iw]);\nconst y = d3.scaleLinear().domain([yExtent[0] - yPad, yExtent[1] + yPad]).range([ih, 0]);\nconst color = d3.scaleOrdinal().domain(clusterDefs.map((c) => c.name)).range(t.palette);\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// --- Cluster hull outlines (d3.polygonHull) --------------------------------\n// A genuine D3-distinctive technique: trace each cluster's convex hull in\n// pixel space to give the grouping a deliberate visual boundary/focal shape\n// beyond color alone, drawn beneath the points so markers stay on top.\nconst hullG = g.append(\"g\").attr(\"class\", \"hulls\");\nfor (const c of clusterDefs) {\n  const hull = d3.polygonHull(\n    points.filter((p) => p.cluster === c.name).map((p) => [x(p.x), y(p.y)])\n  );\n  if (!hull) continue;\n  hullG\n    .append(\"path\")\n    .attr(\"d\", `M${hull.map((p) => p.join(\",\")).join(\"L\")}Z`)\n    .attr(\"fill\", color(c.name))\n    .attr(\"fill-opacity\", 0.08)\n    .attr(\"stroke\", color(c.name))\n    .attr(\"stroke-opacity\", 0.4)\n    .attr(\"stroke-width\", 1.5)\n    .attr(\"stroke-linejoin\", \"round\");\n}\n\n// --- Points (moderate size + alpha to handle overlap at n=600) -------------\ng.selectAll(\"circle\")\n  .data(points)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d.x))\n  .attr(\"cy\", (d) => y(d.y))\n  .attr(\"r\", 5.5)\n  .attr(\"fill\", (d) => color(d.cluster))\n  .attr(\"fill-opacity\", 0.62)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 0.6);\n\n// --- Centroid labels (cluster name over a soft halo for legibility) --------\nconst centroidG = g\n  .selectAll(\".centroid\")\n  .data(clusters)\n  .join(\"g\")\n  .attr(\"class\", \"centroid\")\n  .attr(\"transform\", (d) => `translate(${x(d.cx)},${y(d.cy)})`);\n\ncentroidG.each(function (d) {\n  const node = d3.select(this);\n  const label = node\n    .append(\"text\")\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"dy\", \"0.35em\")\n    .style(\"font-size\", \"15px\")\n    .style(\"font-weight\", \"600\")\n    .attr(\"fill\", t.ink)\n    .text(d.name);\n  const bbox = label.node().getBBox();\n  node\n    .insert(\"rect\", \"text\")\n    .attr(\"x\", bbox.x - 7)\n    .attr(\"y\", bbox.y - 3)\n    .attr(\"width\", bbox.width + 14)\n    .attr(\"height\", bbox.height + 6)\n    .attr(\"rx\", 4)\n    .attr(\"fill\", t.elevatedBg)\n    .attr(\"fill-opacity\", 0.95)\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 1);\n});\n\n// --- Axis labels (no tick labels — embedding coordinates are not directly\n// interpretable, only relative position and clustering carry meaning) ------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"t-SNE dimension 1\");\n\ng.append(\"text\")\n  .attr(\"transform\", `translate(${-56},${ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"t-SNE dimension 2\");\n\n// --- Legend -----------------------------------------------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left + iw + 50},${margin.top + 30})`);\n\nconst legendItems = legend\n  .selectAll(\".legend-item\")\n  .data(clusterDefs)\n  .join(\"g\")\n  .attr(\"class\", \"legend-item\")\n  .attr(\"transform\", (d, i) => `translate(0,${i * 40})`);\n\nlegendItems\n  .append(\"circle\")\n  .attr(\"r\", 8)\n  .attr(\"cx\", 8)\n  .attr(\"cy\", 0)\n  .attr(\"fill\", (d) => color(d.name))\n  .attr(\"fill-opacity\", 0.62);\n\nlegendItems\n  .append(\"text\")\n  .attr(\"x\", 26)\n  .attr(\"y\", 0)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d.name);\n\n// --- Title + subtitle --------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"scatter-embedding · javascript · d3 · anyplot.ai\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 82)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(\"NLP document embeddings · t-SNE (perplexity=30)\");\n"}