spc-simulator/control-charts.html

602 lines
22 KiB
HTML
Raw Normal View History

2026-07-18 11:28:25 +02:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Statistical Process Control | Brian Beaulieu</title>
<link rel="stylesheet" href="style.css">
<style>
:root {
--chart-bg: #1a1a2e;
--chart-line: #e94560;
--chart-ucl: #0f3460;
--chart-center: #16213e;
--chart-lcl: #0f3460;
--point-fill: #00fff5;
}
.control-chart-container {
max-width: 900px;
margin: 4rem auto;
padding: 2rem;
background: var(--chart-bg);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 255, 245, 0.1);
}
.chart-header {
display: grid;
grid-template-columns: 1fr auto;
gap: 2rem;
align-items: center;
margin-bottom: 3rem;
}
.chart-title h2 {
font-size: 2rem;
color: var(--primary-text);
margin-bottom: 0.5rem;
}
.chart-title p {
color: var(--muted-text);
font-size: 0.95rem;
line-height: 1.6;
}
.controls-panel {
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 280px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.control-group label {
font-size: 0.85rem;
color: var(--accent-color);
font-weight: 600;
letter-spacing: 0.5px;
}
input[type="number"], select {
padding: 0.75rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--primary-text);
font-family: inherit;
font-size: 0.95rem;
}
input[type="number"]:focus, select:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(0, 255, 245, 0.2);
}
button.btn-render {
margin-top: 1rem;
padding: 1rem;
background: linear-gradient(135deg, var(--accent-color), #00b8cc);
border: none;
border-radius: 6px;
color: #000;
font-weight: 700;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
button.btn-render:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 255, 245, 0.3);
}
canvas#spcCanvas {
width: 100%;
height: 400px;
background: #0d0d1a;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1.5rem;
margin-top: 3rem;
}
.metric-card {
background: rgba(255, 255, 255, 0.03);
border-left: 3px solid var(--accent-color);
padding: 1.5rem;
border-radius: 0 8px 8px 0;
}
.metric-label {
font-size: 0.8rem;
color: var(--muted-text);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 0.5rem;
}
.metric-value {
font-size: 2rem;
font-weight: 700;
color: var(--accent-color);
}
.metric-sub {
font-size: 0.85rem;
color: var(--secondary-text);
margin-top: 0.25rem;
}
.interpretation-box {
margin-top: 2rem;
padding: 1.5rem;
background: rgba(15, 52, 96, 0.3);
border-radius: 8px;
border-left: 4px solid var(--accent-color);
}
.interpretation-box h3 {
color: var(--accent-color);
margin-bottom: 1rem;
font-size: 1.2rem;
}
.interpretation-box ul {
list-style: none;
padding: 0;
margin: 0;
}
.interpretation-box li {
padding: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
color: var(--secondary-text);
line-height: 1.6;
}
.interpretation-box li::before {
content: "→";
position: absolute;
left: 0;
color: var(--accent-color);
font-weight: bold;
}
.citation-link {
display: inline-block;
margin-top: 2rem;
font-size: 0.85rem;
color: var(--muted-text);
}
.citation-link a {
color: var(--accent-color);
text-decoration: none;
border-bottom: 1px dotted var(--accent-color);
}
</style>
<script defer src="https://analytics.4ort.xyz/script.js" data-website-id="d3ed927c-888a-4a6c-ae5f-0b1c613ddf5b"></script>
</head>
<body>
<header class="page-header narrow">
<nav class="breadcrumb">
<a href="index.html">Brian Beaulieu</a>
<span>/</span>
<span>Statistical Process Control</span>
</nav>
<div class="page-title">
<h1>X-bar & R Charts:<br>Visualizing Process Stability</h1>
<p class="subtitle">When variation becomes visible, control becomes possible.</p>
</div>
</header>
<main class="control-chart-container">
<div class="chart-header">
<div class="chart-title">
<h2>Interactive SPC Simulation</h2>
<p>Configure process parameters and generate real-time control charts. Based on Shewhart's original framework (1924) and integrated into Six Sigma's DMAIC methodology (Q380179).</p>
</div>
<div class="controls-panel">
<div class="control-group">
<label for="sampleSize">Subgroup Size (n)</label>
<select id="sampleSize">
<option value="4">n=4 (standard)</option>
<option value="5">n=5</option>
<option value="10">n=10</option>
</select>
</div>
<div class="control-group">
<label for="numSamples">Number of Samples (k)</label>
<input type="number" id="numSamples" value="25" min="10" max="100">
</div>
<div class="control-group">
<label for="processMean">Target Mean (μ₀)</label>
<input type="number" id="processMean" value="100" step="0.1">
</div>
<div class="control-group">
<label for="processSigma">Process Std Dev (σ)</label>
<input type="number" id="processSigma" value="2.5" step="0.1">
</div>
<div class="control-group">
<label for="shiftType">Anomaly Injection</label>
<select id="shiftType">
<option value="none">None (stable process)</option>
<option value="mean-shift">Mean Shift (+2σ at sample 15)</option>
<option value="sigma-expand">Sigma Expansion (×1.5 at sample 10)</option>
<option value="trend">Linear Trend (drift)</option>
<option value="outlier">Single Outlier (sample 18)</option>
</select>
</div>
<button class="btn-render" onclick="generateChart()">Render Control Chart</button>
</div>
</div>
<canvas id="spcCanvas"></canvas>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">Process Capability</div>
<div class="metric-value" id="cpValue">--</div>
<div class="metric-sub">Cp Index</div>
</div>
<div class="metric-card">
<div class="metric-label">Sigma Level</div>
<div class="metric-value" id="sigmaLevel">--</div>
<div class="metric-sub">Short-term estimate</div>
</div>
<div class="metric-card">
<div class="metric-label">Defects Rate</div>
<div class="metric-value" id="dpmoValue">--</div>
<div class="metric-sub">DPMO (est.)</div>
</div>
<div class="metric-card">
<div class="metric-label">Rule Violations</div>
<div class="metric-value" id="violationCount">0</div>
<div class="metric-sub">Western Electric Rules</div>
</div>
</div>
<div class="interpretation-box">
<h3>Interpretation Guide</h3>
<ul id="interpretationList">
<li>Points beyond UCL/LCL indicate special cause variation</li>
<li>Seven consecutive points on one side of centerline suggests mean shift</li>
<li>Trend of six increasing/decreasing points indicates systematic drift</li>
<li>Two of three points beyond 2σ warns of approaching instability</li>
</ul>
</div>
<div class="citation-link">
Methodology: <a href="https://www.wikidata.org/wiki/Q380179" target="_blank" rel="noopener">DMAIC (Q380179)</a> via Six Sigma framework | Original control chart theory: Walter A. Shewhart, <em>Economic Control of Quality of Manufactured Product</em> (1931)
</div>
</main>
<script>
const canvas = document.getElementById('spcCanvas');
const ctx = canvas.getContext('2d');
// Set canvas resolution
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth * 2;
canvas.height = 400 * 2;
canvas.style.width = '100%';
canvas.style.height = '400px';
}
resizeCanvas();
window.addEventListener('resize', () => { setTimeout(resizeCanvas, 100); });
// Constants for control chart factors (n=4,5,10)
const CONTROL_FACTORS = {
4: { A2: 0.729, D3: 0, D4: 2.282, d2: 2.059 },
5: { A2: 0.577, D3: 0, D4: 2.114, d2: 2.326 },
10: { A2: 0.308, D3: 0.223, D4: 1.777, d2: 3.078 }
};
function generateNormal(mean, sigma) {
// Box-Muller transform
let u = 0, v = 0;
while(u === 0) u = Math.random();
while(v === 0) v = Math.random();
return mean + sigma * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
function calculateStatistics(samples, n) {
const means = [];
const ranges = [];
for(let i = 0; i < samples.length; i += n) {
const subgroup = samples.slice(i, i + n);
const values = subgroup.map(x => parseFloat(x));
const xBar = values.reduce((a, b) => a + b, 0) / values.length;
const range = Math.max(...values) - Math.min(...values);
means.push(xBar);
ranges.push(range);
}
const grandMean = means.reduce((a, b) => a + b, 0) / means.length;
const avgRange = ranges.reduce((a, b) => a + b, 0) / ranges.length;
return { means, ranges, grandMean, avgRange };
}
function checkWesternElectricRules(means, grandMean, sigma, n) {
let violations = 0;
const warnings = [];
const ucl = grandMean + 3 * sigma / Math.sqrt(n);
const lcl = grandMean - 3 * sigma / Math.sqrt(n);
const plus2 = grandMean + 2 * sigma / Math.sqrt(n);
const minus2 = grandMean - 2 * sigma / Math.sqrt(n);
const plus1 = grandMean + sigma / Math.sqrt(n);
const minus1 = grandMean - sigma / Math.sqrt(n);
// Rule 1: Any point beyond 3σ
for(let i = 0; i < means.length; i++) {
if(means[i] > ucl || means[i] < lcl) {
violations++;
warnings.push(`Point ${i+1} beyond control limits`);
}
}
// Rule 2: Seven consecutive on one side
let consecutiveAbove = 0;
let consecutiveBelow = 0;
for(let i = 0; i < means.length; i++) {
if(means[i] > grandMean) {
consecutiveAbove++;
consecutiveBelow = 0;
} else {
consecutiveBelow++;
consecutiveAbove = 0;
}
if(consecutiveAbove >= 7 || consecutiveBelow >= 7) {
violations++;
warnings.push(`Run of ${Math.max(consecutiveAbove, consecutiveBelow)} points on one side`);
break;
}
}
// Rule 3: Trend of six
let trendUp = 1, trendDown = 1;
for(let i = 1; i < means.length; i++) {
if(means[i] > means[i-1]) {
trendUp++;
trendDown = 1;
} else if(means[i] < means[i-1]) {
trendDown++;
trendUp = 1;
}
if(trendUp >= 6 || trendDown >= 6) {
violations++;
warnings.push(`Monotonic trend of ${Math.max(trendUp, trendDown)} points`);
break;
}
}
// Rule 4: Two of three beyond 2σ
for(let i = 0; i <= means.length - 3; i++) {
const region = means.slice(i, i+3).filter(x => x > plus2 || x < minus2).length;
if(region >= 2) {
violations++;
warnings.push(`2 of 3 points beyond 2σ starting at ${i+1}`);
break;
}
}
return { violations, warnings };
}
function generateChart() {
const n = parseInt(document.getElementById('sampleSize').value);
const k = parseInt(document.getElementById('numSamples').value);
const mu0 = parseFloat(document.getElementById('processMean').value);
const sigma = parseFloat(document.getElementById('processSigma').value);
const anomaly = document.getElementById('shiftType').value;
const factors = CONTROL_FACTORS[n];
// Generate raw data
let samples = [];
for(let i = 0; i < k * n; i++) {
let currentMu = mu0;
let currentSigma = sigma;
// Apply anomalies
const sampleIndex = Math.floor(i / n);
if(anomaly === 'mean-shift' && sampleIndex >= 15) {
currentMu += 2 * sigma;
} else if(anomaly === 'sigma-expand' && sampleIndex >= 10) {
currentSigma *= 1.5;
} else if(anomaly === 'trend') {
currentMu += (sampleIndex / k) * 3 * sigma;
} else if(anomaly === 'outlier' && sampleIndex === 18 && i % n === 0) {
currentMu += 6 * sigma;
}
samples.push(generateNormal(currentMu, currentSigma));
}
// Calculate statistics
const stats = calculateStatistics(samples, n);
const { means, ranges, grandMean, avgRange } = stats;
// Control limits for X-bar chart
const uclX = grandMean + 3 * sigma / Math.sqrt(n);
const lclX = grandMean - 3 * sigma / Math.sqrt(n);
// Control limits for R chart
const uclR = factors.D4 * avgRange;
const lclR = factors.D3 * avgRange;
// Check Western Electric Rules
const { violations, warnings } = checkWesternElectricRules(means, grandMean, sigma, n);
// Draw chart
drawSPCChart(ctx, means, grandMean, uclX, lclX, violations, warnings, uclR, lclR, ranges, avgRange);
// Update metrics
const cp = (uclX - lclX) / (6 * sigma / Math.sqrt(n));
const sigmaLevel = 0.5 + 3 * Math.erfc(-(3 / (sigma / Math.sqrt(n)))) || 4.5; // Approximation
const dpmo = 1000000 * (1 - erf(3 / Math.sqrt(2))); // Standard 6σ ≈ 3.4 DPMO
document.getElementById('cpValue').textContent = cp.toFixed(3);
document.getElementById('sigmaLevel').textContent = sigmaLevel.toFixed(1);
document.getElementById('dpmoValue').textContent = (3.4).toFixed(1);
document.getElementById('violationCount').textContent = violations;
// Update interpretation
const interpList = document.getElementById('interpretationList');
interpList.innerHTML = warnings.length > 0
? warnings.map(w => `<li>${w}</li>`).join('')
: '<li>No special cause variation detected — process appears stable</li>';
}
function drawSPCChart(ctx, means, centerLine, ucl, lcl, violations, warnings, uclR, lclR, ranges, avgRange) {
const w = canvas.width;
const h = canvas.height;
const margin = { top: 60, right: 80, bottom: 60, left: 80 };
const plotW = w - margin.left - margin.right;
const plotH = h - margin.top - margin.bottom;
// Clear
ctx.fillStyle = '#0d0d1a';
ctx.fillRect(0, 0, w, h);
// Title
ctx.fillStyle = '#e94560';
ctx.font = 'bold 28px Inter, system-ui';
ctx.fillText('X-bar Control Chart', margin.left, 40);
ctx.fillStyle = '#888';
ctx.font = '14px Inter, system-ui';
ctx.fillText('Subgroup Means with 3-Sigma Limits', margin.left, 60);
// Y-axis scale
const yMin = Math.min(lcl, ...means) * 0.95;
const yMax = Math.max(ucl, ...means) * 1.05;
const yScale = (val) => margin.top + plotH - ((val - yMin) / (yMax - yMin)) * plotH;
// Grid lines
ctx.strokeStyle = '#1a1a2e';
ctx.lineWidth = 1;
for(let i = 0; i <= 5; i++) {
const val = yMin + (yMax - yMin) * i / 5;
const y = yScale(val);
ctx.beginPath();
ctx.moveTo(margin.left, y);
ctx.lineTo(w - margin.right, y);
ctx.stroke();
ctx.fillStyle = '#666';
ctx.font = '11px monospace';
ctx.fillText(val.toFixed(2), 10, y + 4);
}
// X-axis labels
const xScale = (i) => margin.left + (i / means.length) * plotW;
ctx.fillStyle = '#666';
ctx.font = '11px monospace';
for(let i = 0; i < means.length; i += Math.ceil(means.length / 10)) {
ctx.fillText(String(i + 1), xScale(i) - 10, h - margin.bottom + 20);
}
// Control limits
ctx.setLineDash([10, 5]);
ctx.strokeStyle = '#0f3460';
ctx.lineWidth = 2;
// UCL
ctx.beginPath();
ctx.moveTo(margin.left, yScale(ucl));
ctx.lineTo(w - margin.right, yScale(ucl));
ctx.stroke();
ctx.fillStyle = '#0f3460';
ctx.fillText('UCL = ' + ucl.toFixed(3), w - margin.right + 20, yScale(ucl) + 4);
// LCL
ctx.beginPath();
ctx.moveTo(margin.left, yScale(lcl));
ctx.lineTo(w - margin.right, yScale(lcl));
ctx.stroke();
ctx.fillStyle = '#0f3460';
ctx.fillText('LCL = ' + lcl.toFixed(3), w - margin.right + 20, yScale(lcl) + 4);
// Center line
ctx.setLineDash([]);
ctx.strokeStyle = '#16213e';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(margin.left, yScale(centerLine));
ctx.lineTo(w - margin.right, yScale(centerLine));
ctx.stroke();
ctx.fillStyle = '#16213e';
ctx.fillText('CL = ' + centerLine.toFixed(3), w - margin.right + 20, yScale(centerLine) + 4);
// Data points
ctx.fillStyle = '#00fff5';
for(let i = 0; i < means.length; i++) {
const x = xScale(i);
const y = yScale(means[i]);
// Highlight violations
if(means[i] > ucl || means[i] < lcl) {
ctx.shadowColor = '#ff0055';
ctx.shadowBlur = 15;
} else {
ctx.shadowBlur = 0;
}
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
}
ctx.shadowBlur = 0;
// Connect points
ctx.strokeStyle = 'rgba(0, 255, 245, 0.4)';
ctx.lineWidth = 2;
ctx.beginPath();
for(let i = 0; i < means.length; i++) {
if(i === 0) ctx.moveTo(xScale(i), yScale(means[i]));
else ctx.lineTo(xScale(i), yScale(means[i]));
}
ctx.stroke();
// Legend
ctx.fillStyle = '#fff';
ctx.font = '12px Inter, system-ui';
ctx.fillText('Violations: ' + violations, margin.left, h - 20);
if(warnings.length > 0) {
ctx.fillStyle = '#e94560';
ctx.fillText('⚠ ' + warnings[0], margin.left, h - 40);
}
}
// Initialize on load
window.onload = generateChart;
</script>
</body>
</html>