602 lines
22 KiB
HTML
602 lines
22 KiB
HTML
|
|
<!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>
|