{"spec_id":"strip-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// strip-basic: Basic Strip Plot\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 89/100 | Created: 2026-08-05\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction randNormal(mean, std) {\n  const u1 = Math.max(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 * std;\n}\n\nconst batches = [\n  { name: \"Batch 1\", mean: 12.0, std: 0.04 },\n  { name: \"Batch 2\", mean: 12.02, std: 0.05 },\n  { name: \"Batch 3\", mean: 11.97, std: 0.09 },\n  { name: \"Batch 4\", mean: 12.01, std: 0.03 },\n];\nconst pointsPerBatch = 60;\nconst data = [];\nfor (const b of batches) {\n  for (let i = 0; i < pointsPerBatch; i++) {\n    data.push({ category: b.name, value: randNormal(b.mean, b.std) });\n  }\n}\n\n// --- Layout -------------------------------------------------------------\nconst margin = { top: 120, right: 60, bottom: 110, left: 130 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\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  .scaleBand()\n  .domain(batches.map((b) => b.name))\n  .range([0, iw])\n  .padding(0.4);\nconst y = d3\n  .scaleLinear()\n  .domain(d3.extent(data, (d) => d.value))\n  .nice()\n  .range([ih, 0]);\nconst color = d3\n  .scaleOrdinal()\n  .domain(batches.map((b) => b.name))\n  .range(t.palette);\n\n// --- Gridlines (y-axis only, scatter-style plot) -----------------------------\nconst grid = g.append(\"g\").attr(\"class\", \"grid\").call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"));\ngrid.select(\".domain\").remove();\ngrid.selectAll(\"line\").attr(\"stroke\", t.grid);\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x).tickSizeOuter(0));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickFormat(d3.format(\".2f\")).tickSizeOuter(0));\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}\ng.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\ngrid.selectAll(\".tick line\").attr(\"stroke\", t.grid);\n\n// --- Jittered strip points (D3 force-simulation beeswarm) --------------------\n// True values stay pinned on y (fy); a collision + centering force resolves x\n// so points spread just enough to avoid overlap, instead of plain uniform jitter.\nconst xCenter = (name) => x(name) + x.bandwidth() / 2;\nconst jitterWidth = x.bandwidth() * 0.6;\nconst pointRadius = 4;\n\nconst nodes = data.map((d) => ({\n  ...d,\n  x: xCenter(d.category) + (rand() - 0.5) * jitterWidth,\n  y: y(d.value),\n  fy: y(d.value),\n}));\n\nd3.forceSimulation(nodes)\n  .force(\"x\", d3.forceX((d) => xCenter(d.category)).strength(0.08))\n  .force(\"collide\", d3.forceCollide(pointRadius + 0.6))\n  .stop()\n  .tick(150);\n\nfor (const n of nodes) {\n  const lo = xCenter(n.category) - jitterWidth / 2;\n  const hi = xCenter(n.category) + jitterWidth / 2;\n  n.x = Math.min(hi, Math.max(lo, n.x));\n}\n\ng.selectAll(\"circle\")\n  .data(nodes)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => d.x)\n  .attr(\"cy\", (d) => d.y)\n  .attr(\"r\", pointRadius)\n  .attr(\"fill\", (d) => color(d.category))\n  .attr(\"fill-opacity\", 0.55)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 0.5);\n\n// --- Mean reference lines (data storytelling: highlights Batch 3's shift) ----\nfor (const b of batches) {\n  const mean = d3.mean(data, (d) => (d.category === b.name ? d.value : undefined));\n  g.append(\"line\")\n    .attr(\"x1\", x(b.name) + x.bandwidth() * 0.08)\n    .attr(\"x2\", x(b.name) + x.bandwidth() * 0.92)\n    .attr(\"y1\", y(mean))\n    .attr(\"y2\", y(mean))\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 2)\n    .attr(\"stroke-dasharray\", \"6,4\")\n    .attr(\"opacity\", 0.85);\n}\n\ng.append(\"text\")\n  .attr(\"x\", 0)\n  .attr(\"y\", -14)\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-style\", \"italic\")\n  .text(\"Dashed line = batch mean\");\n\n// --- Axis labels --------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Production Batch\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -90)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Shaft Diameter (mm)\");\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .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\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"strip-basic · javascript · d3 · anyplot.ai\");\n"}