{"spec_id":"rug-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// rug-basic: Basic Rug Plot\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 96/100 | Created: 2026-07-25\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 60, bottom: 110, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// API endpoint response times (ms) for 180 requests: a lognormal-ish cluster\n// of fast responses plus a small cluster of slow/timed-out retries.\nlet seed = 42;\nconst lcg = () => {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n};\nconst gaussian = () => {\n  const u1 = lcg() || 1e-9;\n  const u2 = lcg();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n};\n\nconst mainCluster = [];\nfor (let i = 0; i < 170; i++) {\n  mainCluster.push(Math.exp(Math.log(140) + 0.35 * gaussian()));\n}\nconst retryCluster = [];\nfor (let i = 0; i < 10; i++) {\n  retryCluster.push(500 + gaussian() * 18);\n}\nconst responseTimes = mainCluster.concat(retryCluster);\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// --- Layout: histogram on top, rug strip below, sharing the x scale ----------\n// rugGap holds a tinted \"lane\" band (drawn below) that visually ties the rug\n// strip to the histogram above it, plus its caption.\nconst rugHeight = 34;\nconst rugGap = 30;\nconst histHeight = ih - rugHeight - rugGap;\nconst rugTop = histHeight + rugGap;\n\n// --- Scale ----------------------------------------------------------------\nconst x = d3\n  .scaleLinear()\n  .domain([0, d3.max(responseTimes) * 1.04])\n  .range([0, iw]);\n\nconst bins = d3.bin().domain(x.domain()).thresholds(24)(responseTimes);\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max(bins, (b) => b.length)])\n  .nice()\n  .range([histHeight, 0]);\n\n// --- Y gridlines (histogram area only) --------------------------------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .call((axis) => axis.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Histogram bars -----------------------------------------------------------\ng.selectAll(\"rect.bar\")\n  .data(bins)\n  .join(\"rect\")\n  .attr(\"class\", \"bar\")\n  .attr(\"x\", (b) => x(b.x0) + 1)\n  .attr(\"y\", (b) => y(b.length))\n  .attr(\"width\", (b) => Math.max(0, x(b.x1) - x(b.x0) - 2))\n  .attr(\"height\", (b) => histHeight - y(b.length))\n  .attr(\"rx\", 2)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.85)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1);\n\n// --- Storytelling callouts: name the gap and the retry cluster the rug ------\n// reveals but the histogram bins hide.\nconst mainMax = d3.max(mainCluster);\nconst retryMin = d3.min(retryCluster);\nconst gapMidX = x((mainMax + retryMin) / 2);\nconst retryMidX = x(d3.mean(retryCluster));\n\ng.append(\"text\")\n  .attr(\"x\", gapMidX)\n  .attr(\"y\", 16)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .text(\"gap\");\ng.append(\"line\")\n  .attr(\"x1\", gapMidX)\n  .attr(\"x2\", gapMidX)\n  .attr(\"y1\", 22)\n  .attr(\"y2\", histHeight)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-dasharray\", \"2,3\")\n  .attr(\"stroke-opacity\", 0.5);\n\ng.append(\"text\")\n  .attr(\"x\", retryMidX)\n  .attr(\"y\", 16)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .text(\"slow retries\");\ng.append(\"line\")\n  .attr(\"x1\", retryMidX)\n  .attr(\"x2\", retryMidX)\n  .attr(\"y1\", 22)\n  .attr(\"y2\", rugTop - 10)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-dasharray\", \"2,3\")\n  .attr(\"stroke-opacity\", 0.5);\n\n// --- Rug lane: tinted band (same hue as the data) links it to the histogram --\ng.append(\"rect\")\n  .attr(\"x\", 0)\n  .attr(\"y\", rugTop - 10)\n  .attr(\"width\", iw)\n  .attr(\"height\", rugHeight + 20)\n  .attr(\"rx\", 6)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.07);\n\ng.append(\"text\")\n  .attr(\"x\", iw)\n  .attr(\"y\", rugTop - 14)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"11px\")\n  .text(\"Individual requests\");\n\n// --- Rug strip: one tick per observation ------------------------------------\n// D3-specific touch: opacity is derived per-tick from local pixel density (a\n// fixed-radius neighbor count), so the crowded core fades enough to stay\n// readable as density rather than merging into a solid band, while isolated\n// ticks in the sparse regions stay strongly visible.\nconst rugRadiusPx = 3.5;\nconst px = responseTimes.map((d) => x(d));\nconst density = px.map((xi) => px.reduce((n, xj) => n + (Math.abs(xi - xj) <= rugRadiusPx ? 1 : 0), 0));\nconst tickOpacity = d3.scaleLinear().domain(d3.extent(density)).range([0.55, 0.16]).clamp(true);\n\ng.selectAll(\"line.rug\")\n  .data(responseTimes)\n  .join(\"line\")\n  .attr(\"class\", \"rug\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", rugTop)\n  .attr(\"y2\", rugTop + rugHeight)\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-opacity\", (d, i) => tickOpacity(density[i]));\n\n// --- Axes ------------------------------------------------------------------\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6));\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${rugTop + rugHeight})`)\n  .call(d3.axisBottom(x));\n\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels -------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", rugTop + rugHeight + 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Response Time (ms)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -histHeight / 2)\n  .attr(\"y\", -70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Number of Requests\");\n\n// --- Title --------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"rug-basic · javascript · d3 · anyplot.ai\");\n"}