{"spec_id":"spc-xbar-r","library":"d3","language":"javascript","code":"// anyplot.ai\n// spc-xbar-r: Statistical Process Control Chart (X-bar/R)\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-20\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// SPC constants for subgroup size n=5; D3=0 so LCL_R excluded\nconst A2 = 0.577, D4 = 2.114;\n\n// Park-Miller LCG for deterministic, reproducible data\nlet _s = 42;\nfunction rng() {\n  _s = (_s * 16807) % 2147483647;\n  return _s / 2147483647;\n}\nfunction randn() {\n  return Math.sqrt(-2 * Math.log(rng())) * Math.cos(2 * Math.PI * rng());\n}\n\n// Generate 30 subgroups of n=5 shaft diameter measurements (mm)\nconst PROC_MEAN = 25.0, SIGMA_W = 0.012, N_SUB = 5, N_SAMP = 30;\n\nconst rawSamples = Array.from({ length: N_SAMP }, (_, i) => {\n  const vals = Array.from({ length: N_SUB }, () => PROC_MEAN + SIGMA_W * randn());\n  return {\n    id: i + 1,\n    mean: vals.reduce((a, b) => a + b) / N_SUB,\n    range: Math.max(...vals) - Math.min(...vals),\n  };\n});\n\n// Baseline control limits (computed from all in-control samples)\nconst xbarBar = rawSamples.reduce((a, d) => a + d.mean, 0) / N_SAMP;\nconst rBar    = rawSamples.reduce((a, d) => a + d.range, 0) / N_SAMP;\nconst xbarUCL = xbarBar + A2 * rBar;\nconst xbarLCL = xbarBar - A2 * rBar;\nconst xbarUWL = xbarBar + A2 * rBar * (2 / 3);\nconst xbarLWL = xbarBar - A2 * rBar * (2 / 3);\nconst rUCL    = D4 * rBar;\nconst rUWL    = rBar + (D4 - 1) * rBar * (2 / 3);\n\n// Clone and inject 3 out-of-control signals\nconst samples = rawSamples.map(d => ({ ...d }));\nsamples[7].mean   = xbarUCL + 0.003;  // sample 8  — X̄ above UCL\nsamples[18].mean  = xbarLCL - 0.004;  // sample 19 — X̄ below LCL\nsamples[24].range = rUCL    + 0.002;  // sample 25 — R above UCL\n\nsamples.forEach(d => {\n  d.xOOC = d.mean  > xbarUCL || d.mean  < xbarLCL;\n  d.rOOC = d.range > rUCL;\n});\n\n// Layout — increased top margin for proper legend-to-panel breathing room\nconst margin = { top: 108, right: 90, bottom: 52, left: 90 };\nconst gap    = 32;\nconst panelH = Math.floor((height - margin.top - margin.bottom - gap) / 2);\nconst iw     = width - margin.left - margin.right;\n\n// Imprint palette colors\nconst DATA_COLOR  = t.palette[0];  // #009E73 brand green — in-control data\nconst OOC_COLOR   = t.palette[4];  // #AE3030 matte red  — out-of-control\nconst LIMIT_COLOR = t.palette[2];  // #4467A3 blue       — UCL/LCL lines\nconst WARN_COLOR  = t.palette[3];  // #BD8233 ochre      — ±2σ warning limits\n\n// SVG root\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\n\n// Chart title\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"spc-xbar-r · javascript · d3 · anyplot.ai\");\n\n// Shared x-scale across both panels (discrete sample IDs)\nconst xScale = d3.scalePoint()\n  .domain(samples.map(d => d.id))\n  .range([0, iw])\n  .padding(0.5);\n\n// Horizontal legend row between title and panels\nconst legendItems = [\n  { color: DATA_COLOR,  type: \"circle\",    label: \"In-control\" },\n  { color: OOC_COLOR,   type: \"circle-lg\", label: \"Out-of-control\" },\n  { color: t.ink,       type: \"solid\",     label: \"Center line\" },\n  { color: LIMIT_COLOR, type: \"dash\",      label: \"Control limits (±3σ)\" },\n  { color: WARN_COLOR,  type: \"warn\",      label: \"Warning limits (±2σ)\" },\n];\nconst legStep  = 195;\nconst legStart = (width - legendItems.length * legStep) / 2;\nconst legY     = 70;\n\n// Elevated background behind legend row\nsvg.append(\"rect\")\n  .attr(\"x\", legStart - 14).attr(\"y\", legY - 18)\n  .attr(\"width\", legendItems.length * legStep + 28).attr(\"height\", 36)\n  .attr(\"fill\", t.elevatedBg).attr(\"rx\", 4);\n\nlegendItems.forEach((item, i) => {\n  const lx = legStart + i * legStep;\n  if (item.type === \"circle\") {\n    svg.append(\"circle\").attr(\"cx\", lx + 8).attr(\"cy\", legY)\n      .attr(\"r\", 5).attr(\"fill\", item.color).attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 1.5);\n  } else if (item.type === \"circle-lg\") {\n    svg.append(\"circle\").attr(\"cx\", lx + 8).attr(\"cy\", legY)\n      .attr(\"r\", 7.5).attr(\"fill\", item.color).attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 2);\n  } else if (item.type === \"solid\") {\n    svg.append(\"line\").attr(\"x1\", lx).attr(\"x2\", lx + 22)\n      .attr(\"y1\", legY).attr(\"y2\", legY)\n      .attr(\"stroke\", item.color).attr(\"stroke-width\", 2).attr(\"opacity\", 0.8);\n  } else if (item.type === \"dash\") {\n    svg.append(\"line\").attr(\"x1\", lx).attr(\"x2\", lx + 22)\n      .attr(\"y1\", legY).attr(\"y2\", legY)\n      .attr(\"stroke\", item.color).attr(\"stroke-width\", 2.2).attr(\"stroke-dasharray\", \"7,4\");\n  } else if (item.type === \"warn\") {\n    svg.append(\"line\").attr(\"x1\", lx).attr(\"x2\", lx + 22)\n      .attr(\"y1\", legY).attr(\"y2\", legY)\n      .attr(\"stroke\", item.color).attr(\"stroke-width\", 1.5)\n      .attr(\"stroke-dasharray\", \"5,3\").attr(\"opacity\", 0.65);\n  }\n  svg.append(\"text\").attr(\"x\", lx + 28).attr(\"y\", legY + 5)\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\").text(item.label);\n});\n\n// Draw a single SPC panel (X̄ or R)\nfunction drawPanel(panelTop, yScale, getVal, isOOC, opts) {\n  const g = svg.append(\"g\")\n    .attr(\"transform\", `translate(${margin.left},${panelTop})`);\n\n  // Y gridlines\n  yScale.ticks(5).forEach(tick => {\n    g.append(\"line\")\n      .attr(\"x1\", 0).attr(\"x2\", iw)\n      .attr(\"y1\", yScale(tick)).attr(\"y2\", yScale(tick))\n      .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n  });\n\n  // Upper warning limit\n  if (opts.uwl !== undefined) {\n    const wy = yScale(opts.uwl);\n    g.append(\"line\").attr(\"x1\", 0).attr(\"x2\", iw).attr(\"y1\", wy).attr(\"y2\", wy)\n      .attr(\"stroke\", WARN_COLOR).attr(\"stroke-width\", 1.5)\n      .attr(\"stroke-dasharray\", \"5,4\").attr(\"opacity\", 0.65);\n    g.append(\"text\").attr(\"x\", iw + 6).attr(\"y\", wy + 4)\n      .attr(\"fill\", WARN_COLOR).style(\"font-size\", \"13px\").text(\"+2σ\");\n  }\n\n  // Lower warning limit\n  if (opts.lwl !== undefined) {\n    const ly = yScale(opts.lwl);\n    g.append(\"line\").attr(\"x1\", 0).attr(\"x2\", iw).attr(\"y1\", ly).attr(\"y2\", ly)\n      .attr(\"stroke\", WARN_COLOR).attr(\"stroke-width\", 1.5)\n      .attr(\"stroke-dasharray\", \"5,4\").attr(\"opacity\", 0.65);\n    g.append(\"text\").attr(\"x\", iw + 6).attr(\"y\", ly + 4)\n      .attr(\"fill\", WARN_COLOR).style(\"font-size\", \"13px\").text(\"-2σ\");\n  }\n\n  // UCL\n  const uy = yScale(opts.ucl);\n  g.append(\"line\").attr(\"x1\", 0).attr(\"x2\", iw).attr(\"y1\", uy).attr(\"y2\", uy)\n    .attr(\"stroke\", LIMIT_COLOR).attr(\"stroke-width\", 2.2).attr(\"stroke-dasharray\", \"8,5\");\n  g.append(\"text\").attr(\"x\", iw + 6).attr(\"y\", uy + 4)\n    .attr(\"fill\", LIMIT_COLOR).style(\"font-size\", \"12px\").style(\"font-weight\", \"600\").text(\"UCL\");\n\n  // LCL (X̄ chart only — R chart LCL = 0 for n=5, not plotted)\n  if (opts.showLcl) {\n    const lcy = yScale(opts.lcl);\n    g.append(\"line\").attr(\"x1\", 0).attr(\"x2\", iw).attr(\"y1\", lcy).attr(\"y2\", lcy)\n      .attr(\"stroke\", LIMIT_COLOR).attr(\"stroke-width\", 2.2).attr(\"stroke-dasharray\", \"8,5\");\n    g.append(\"text\").attr(\"x\", iw + 6).attr(\"y\", lcy + 4)\n      .attr(\"fill\", LIMIT_COLOR).style(\"font-size\", \"12px\").style(\"font-weight\", \"600\").text(\"LCL\");\n  }\n\n  // Center line — increased opacity for legibility in dark theme\n  const cy = yScale(opts.cl);\n  g.append(\"line\").attr(\"x1\", 0).attr(\"x2\", iw).attr(\"y1\", cy).attr(\"y2\", cy)\n    .attr(\"stroke\", t.ink).attr(\"stroke-width\", 2).attr(\"opacity\", 0.8);\n  g.append(\"text\").attr(\"x\", iw + 6).attr(\"y\", cy + 4)\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\").text(\"CL\");\n\n  // Data line connecting all sample points\n  g.append(\"path\")\n    .datum(samples)\n    .attr(\"fill\", \"none\").attr(\"stroke\", DATA_COLOR).attr(\"stroke-width\", 2.5)\n    .attr(\"d\", d3.line().x(d => xScale(d.id)).y(d => yScale(getVal(d))));\n\n  // In-control points\n  g.selectAll(\".ic\").data(samples.filter(d => !isOOC(d))).join(\"circle\")\n    .attr(\"class\", \"ic\")\n    .attr(\"cx\", d => xScale(d.id)).attr(\"cy\", d => yScale(getVal(d)))\n    .attr(\"r\", 5).attr(\"fill\", DATA_COLOR)\n    .attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 1.5);\n\n  // Out-of-control points (larger, red)\n  g.selectAll(\".oc\").data(samples.filter(d => isOOC(d))).join(\"circle\")\n    .attr(\"class\", \"oc\")\n    .attr(\"cx\", d => xScale(d.id)).attr(\"cy\", d => yScale(getVal(d)))\n    .attr(\"r\", 7.5).attr(\"fill\", OOC_COLOR)\n    .attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 2);\n\n  // OOC event annotations — label each violation with which limit was breached\n  samples.filter(d => isOOC(d)).forEach(d => {\n    const px = xScale(d.id);\n    const py = yScale(getVal(d));\n    const above = getVal(d) > opts.cl;\n    g.append(\"text\")\n      .attr(\"x\", px).attr(\"y\", above ? py - 14 : py + 22)\n      .attr(\"text-anchor\", \"middle\")\n      .attr(\"fill\", OOC_COLOR).style(\"font-size\", \"12px\").style(\"font-weight\", \"700\")\n      .text(above ? \"▲ UCL\" : \"▼ LCL\");\n  });\n\n  // Y axis\n  const ax = g.append(\"g\").call(\n    d3.axisLeft(yScale).ticks(5).tickFormat(d3.format(\".4f\"))\n  );\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n  // Y axis label (rotated)\n  g.append(\"text\")\n    .attr(\"transform\", \"rotate(-90)\")\n    .attr(\"x\", -panelH / 2).attr(\"y\", -72)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.ink).style(\"font-size\", \"14px\").style(\"font-weight\", \"500\")\n    .text(opts.yLabel);\n\n  // Panel header label\n  g.append(\"text\")\n    .attr(\"x\", 10).attr(\"y\", -12)\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\").style(\"font-weight\", \"600\")\n    .text(opts.title);\n}\n\n// X̄ panel (top)\nconst xPad  = (xbarUCL - xbarLCL) * 0.35;\nconst yXbar = d3.scaleLinear()\n  .domain([xbarLCL - xPad, xbarUCL + xPad]).nice()\n  .range([panelH, 0]);\n\ndrawPanel(\n  margin.top, yXbar,\n  d => d.mean, d => d.xOOC,\n  { cl: xbarBar, ucl: xbarUCL, lcl: xbarLCL, showLcl: true,\n    uwl: xbarUWL, lwl: xbarLWL, yLabel: \"X̄ (mm)\", title: \"X̄ Chart\" }\n);\n\n// R panel (bottom)\nconst rPad      = rUCL * 0.28;\nconst rPanelTop = margin.top + panelH + gap;\nconst yR        = d3.scaleLinear()\n  .domain([0, rUCL + rPad]).nice()\n  .range([panelH, 0]);\n\ndrawPanel(\n  rPanelTop, yR,\n  d => d.range, d => d.rOOC,\n  { cl: rBar, ucl: rUCL, lcl: 0, showLcl: false,\n    uwl: rUWL, lwl: undefined, yLabel: \"R (mm)\", title: \"R Chart\" }\n);\n\n// Shared X axis at the bottom of the R panel\nconst xTickVals = samples.filter((_, i) => i % 3 === 0 || i === N_SAMP - 1).map(d => d.id);\nconst gXAxis = svg.append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${rPanelTop + panelH})`);\nconst xAxisG = gXAxis.call(d3.axisBottom(xScale).tickValues(xTickVals));\nxAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\nxAxisG.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// X axis label\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", rPanelTop + panelH + 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"14px\").style(\"font-weight\", \"500\")\n  .text(\"Sample Number\");\n"}