spc-simulator/control-charts.html
2026-07-18 09:28:25 +00:00

602 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>