{"spec_id":"frequency-polygon-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// frequency-polygon-basic: Frequency Polygon for Distribution Comparison\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 70, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Fixed-seed LCG — the browser has no seeded RNG.\nconst lcg = (seed) => {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n};\nconst rng = lcg(42);\nconst randNormal = (mean, sd) => {\n  const u1 = Math.max(rng(), 1e-9);\n  const u2 = rng();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * sd;\n};\n\n// High Load carries a secondary bump: under sustained load a fraction of\n// requests hit retry/timeout queues, producing a long, right-skewed tail\n// on top of the base slowdown — a shape difference plain mean/sd shift\n// can't show, which is exactly what a frequency polygon is good at revealing.\nconst groups = [\n  { name: \"Control\", mean: 450, sd: 55, n: 500, dash: \"0\" },\n  { name: \"Low Load\", mean: 520, sd: 65, n: 500, dash: \"9,5\" },\n  {\n    name: \"High Load\",\n    mean: 590,\n    sd: 70,\n    n: 500,\n    dash: \"2,4\",\n    bump: { mean: 800, sd: 45, weight: 0.22 },\n    focal: true,\n  },\n];\n\nconst binMin = 250;\nconst binMax = 950;\nconst binWidth = 25;\nconst thresholds = d3.range(binMin, binMax + binWidth, binWidth);\nconst bin = d3.bin().domain([binMin, binMax]).thresholds(thresholds);\n\nconst sampleValue = (g) => (g.bump && rng() < g.bump.weight ? randNormal(g.bump.mean, g.bump.sd) : randNormal(g.mean, g.sd));\n\nconst series = groups.map((g, i) => {\n  const values = Array.from({ length: g.n }, () => sampleValue(g)).filter(\n    (v) => v > binMin && v < binMax,\n  );\n  const bins = bin(values);\n  const midpoints = bins.map((b) => ({ x: (b.x0 + b.x1) / 2, y: b.length }));\n  // Extend to zero at both ends to close the polygon shape.\n  const points = [{ x: binMin - binWidth / 2, y: 0 }, ...midpoints, { x: binMax + binWidth / 2, y: 0 }];\n  return { ...g, points, midpoints, color: t.palette[i] };\n});\n\n// --- Scales -------------------------------------------------------------------\nconst allPoints = series.flatMap((s) => s.points);\nconst x = d3\n  .scaleLinear()\n  .domain(d3.extent(allPoints, (d) => d.x))\n  .range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max(allPoints, (d) => d.y)])\n  .nice()\n  .range([ih, 0]);\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// --- Y gridlines (subtle, behind data) -------------------------------------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-opacity\", 0.5);\n\n// --- Area fills + polygon lines + markers ----------------------------------------\nconst area = d3\n  .area()\n  .x((d) => x(d.x))\n  .y0(y(0))\n  .y1((d) => y(d.y))\n  .curve(d3.curveLinear);\n\nconst line = d3\n  .line()\n  .x((d) => x(d.x))\n  .y((d) => y(d.y))\n  .curve(d3.curveLinear);\n\nfor (const s of series) {\n  g.append(\"path\")\n    .datum(s.points)\n    .attr(\"d\", area)\n    .attr(\"fill\", s.color)\n    .attr(\"fill-opacity\", s.focal ? 0.2 : 0.12);\n}\nfor (const s of series) {\n  g.append(\"path\")\n    .datum(s.points)\n    .attr(\"d\", line)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", s.color)\n    .attr(\"stroke-width\", s.focal ? 4.5 : 3.5)\n    .attr(\"stroke-dasharray\", s.dash)\n    .attr(\"stroke-linejoin\", \"round\");\n  g.selectAll(`.marker-${s.name.replace(/\\s+/g, \"\")}`)\n    .data(s.midpoints.filter((d) => d.y > 0))\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\", s.color)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 1.3);\n}\n\n// --- Insight callout on the High Load tail bump (d3.greatest, d3-array) ------------\n// A distinctive, non-generic use of d3 beyond bin/area/line: locate the\n// secondary-bump peak precisely instead of hard-coding its screen position.\nconst highLoad = series.find((s) => s.name === \"High Load\");\nconst tailPeak = d3.greatest(\n  highLoad.midpoints.filter((d) => d.x > 700 && d.y > 0),\n  (d) => d.y,\n);\nif (tailPeak) {\n  const px = x(tailPeak.x);\n  const py = y(tailPeak.y);\n  const lx = px + 18;\n  const ly = py - 68;\n  g.append(\"line\")\n    .attr(\"x1\", px)\n    .attr(\"y1\", py - 8)\n    .attr(\"x2\", lx + 4)\n    .attr(\"y2\", ly + 14)\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1);\n  g.append(\"text\")\n    .attr(\"x\", lx)\n    .attr(\"y\", ly)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"13.5px\")\n    .text(\"High Load: timeout-prone tail\")\n    .append(\"tspan\")\n    .attr(\"x\", lx)\n    .attr(\"dy\", \"1.3em\")\n    .text(\"stretches response times far past the norm\");\n}\n\n// --- Axes -----------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(9));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\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\", \"17px\")\n  .text(\"Response Time (ms)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Frequency (count)\");\n\n// --- Legend (top-right, above the empty tail region) -------------------------------\nconst legend = g.append(\"g\").attr(\"transform\", `translate(${iw - 210},${18})`);\nseries.forEach((s, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0,${i * 30})`);\n  row\n    .append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", 32)\n    .attr(\"y1\", 0)\n    .attr(\"y2\", 0)\n    .attr(\"stroke\", s.color)\n    .attr(\"stroke-width\", 3.5)\n    .attr(\"stroke-dasharray\", s.dash);\n  row\n    .append(\"text\")\n    .attr(\"x\", 42)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(s.name);\n});\n\n// --- Title --------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"700\")\n  .style(\"letter-spacing\", \"0.4px\")\n  .text(\"frequency-polygon-basic · javascript · d3 · anyplot.ai\");\n"}