{"spec_id":"scatter-categorical","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-categorical: Categorical Scatter Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 260, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic fixed-seed LCG) -------------------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction randNormal(mean, sd) {\n  const u1 = rand() || 1e-9;\n  const u2 = rand();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * sd;\n}\n\nconst species = [\n  { name: \"Adelie\", flipperMean: 190, flipperSd: 6.5, massMean: 3700, massSd: 450, n: 45 },\n  { name: \"Chinstrap\", flipperMean: 196, flipperSd: 7, massMean: 3730, massSd: 380, n: 40 },\n  { name: \"Gentoo\", flipperMean: 217, flipperSd: 6, massMean: 5100, massSd: 500, n: 42 },\n];\n\nconst points = [];\nfor (const s of species) {\n  for (let i = 0; i < s.n; i++) {\n    points.push({\n      flipperLength: randNormal(s.flipperMean, s.flipperSd),\n      bodyMass: randNormal(s.massMean, s.massSd),\n      species: s.name,\n    });\n  }\n}\n\nconst categories = species.map((s) => s.name);\nconst color = d3.scaleOrdinal().domain(categories).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// --- Scales ---------------------------------------------------------------\nconst x = d3\n  .scaleLinear()\n  .domain(d3.extent(points, (d) => d.flipperLength)).nice()\n  .range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain(d3.extent(points, (d) => d.bodyMass)).nice()\n  .range([ih, 0]);\n\n// --- Gridlines --------------------------------------------------------------\ng.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).tickSize(-ih).tickFormat(\"\"))\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\ng.append(\"g\")\n  .call(d3.axisLeft(y).ticks(7).tickSize(-iw).tickFormat(\"\"))\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\ng.selectAll(\".domain\").remove();\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\", \"16px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels --------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Flipper Length (mm)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -80)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Body Mass (g)\");\n\n// --- Points ---------------------------------------------------------------\ng.selectAll(\"circle\")\n  .data(points)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d.flipperLength))\n  .attr(\"cy\", (d) => y(d.bodyMass))\n  .attr(\"r\", 6.5)\n  .attr(\"fill\", (d) => color(d.species))\n  .attr(\"fill-opacity\", 0.65)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1);\n\n// --- Centroid markers (data storytelling: per-species mean) -----------------\n// d3-shape symbol generator, not just circles — highlights each species'\n// average flipper length / body mass as a focal point above the raw cloud.\nconst diamond = d3.symbol().type(d3.symbolDiamond).size(450);\nconst centroids = species.map((s) => {\n  const speciesPoints = points.filter((d) => d.species === s.name);\n  return {\n    name: s.name,\n    flipperLength: d3.mean(speciesPoints, (d) => d.flipperLength),\n    bodyMass: d3.mean(speciesPoints, (d) => d.bodyMass),\n  };\n});\n\ng.selectAll(\".centroid\")\n  .data(centroids)\n  .join(\"path\")\n  .attr(\"class\", \"centroid\")\n  .attr(\"d\", diamond)\n  .attr(\"transform\", (d) => `translate(${x(d.flipperLength)},${y(d.bodyMass)})`)\n  .attr(\"fill\", (d) => color(d.name))\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2.5);\n\n// --- Legend -----------------------------------------------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left + iw + 50},${margin.top + 20})`);\n\ncategories.forEach((name, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0,${i * 36})`);\n  row.append(\"circle\").attr(\"r\", 9).attr(\"fill\", color(name)).attr(\"fill-opacity\", 0.75);\n  row\n    .append(\"text\")\n    .attr(\"x\", 20)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"16px\")\n    .text(name);\n});\n\nconst legendNote = legend\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${categories.length * 36 + 14})`);\nlegendNote\n  .append(\"path\")\n  .attr(\"d\", d3.symbol().type(d3.symbolDiamond).size(220))\n  .attr(\"fill\", t.inkSoft)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.5);\nlegendNote\n  .append(\"text\")\n  .attr(\"x\", 20)\n  .attr(\"y\", 5)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Species mean\");\n\n// --- Title ------------------------------------------------------------------\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-categorical · javascript · d3 · anyplot.ai\");\n"}