Component demo · Trends and signals
Control Chart
index.htm as it is written with the smart-industrial package
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Control Chart Overview Demo</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.default.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-elements.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-dark.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.ControlChart is a Shewhart control chart. It shows whether a process has changed, which is a different question from whether the process meets its specification.</h2>
<p>
Control limits are computed from the data: the centre line is the process average and the limits are three sigma on either side, estimated from the within-subgroup spread. Specification limits come from the customer and are drawn in their own style on the primary chart only; they are not used for the control limits or the run rules. All statistics are computed by <code>Smart.Industrial.spc</code>, so a capability figure in a report and the one under the chart are always the same.
</p>
</section>
<section>
<h2>X-bar and R chart: shaft diameter in subgroups of five</h2>
<p>
The limits are established from the first fifteen subgroups and then held. Around subgroup eighteen the process shifts by about half a sigma, which is not enough to put a point outside the limits. Press <em>Add subgroup</em> to continue sampling the shifted process and see the run rules detect the shift before any point is out of control.
</p>
<smart-control-chart id="xbar" type="xbar-r" label="Shaft diameter" unit="mm" precision-digits="3"></smart-control-chart>
<div class="demo-actions">
<button type="button" id="addSubgroup">Add subgroup</button>
<button type="button" id="freeze">Freeze limits</button>
<button type="button" id="reset">Reset</button>
<label>Rules
<select id="rules">
<option value="nelson" selected>Nelson</option>
<option value="westernElectric">Western Electric</option>
<option value="both">Both</option>
<option value="none">None</option>
</select>
</label>
<label><input type="checkbox" id="zones" checked /> Zones</label>
<label><input type="checkbox" id="spec" checked /> Spec limits</label>
<span id="lastEvent" class="stats"></span>
</div>
<p class="hint">
The points that violate a rule and the rule names are listed as text under the chart. The same list is available to screen readers.
</p>
</section>
<section>
<h2>Individuals and moving range chart: fill weight, one unit at a time</h2>
<p>
When a process is measured one unit at a time there are no subgroups, so sigma is estimated from the moving range of successive readings. The moving range chart has one point fewer than the individuals chart.
</p>
<smart-control-chart id="imr" type="i-mr" label="Fill weight" unit="g" precision-digits="1"></smart-control-chart>
</section>
<section>
<h2>p chart: fraction defective with unequal sample sizes</h2>
<p>
A shift that inspects 80 units gives less information than one that inspects 400, so its control limits are wider. The component computes the limits for each sample size and draws them as steps.
</p>
<smart-control-chart id="pchart" type="p" label="Fraction defective" precision-digits="3" show-zones="false"></smart-control-chart>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.controlchart.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* Every chart on this page is fed with data generated here, from a process with a known
mean and sigma, so what the chart reports can be checked against what was put in. The
seam is the `data` property - replace these generators with a query against your
measurement database and nothing else on the page changes. */
/* A seeded generator, so the page shows the same process on every load and the shift
lands where the text says it does. */
function seeded(seed) {
let state = seed >>> 0;
return function () {
state = (state * 1664525 + 1013904223) >>> 0;
return state / 4294967296;
};
}
/* A normal deviate from two uniforms. */
function gaussian(random) {
return Math.sqrt(-2 * Math.log(random() || 1e-12)) * Math.cos(2 * Math.PI * random());
}
window.onload = function () {
/* ── X-bar and R ────────────────────────────────────────────────────── */
const xbar = document.getElementById('xbar'),
addSubgroup = document.getElementById('addSubgroup'),
freeze = document.getElementById('freeze'),
reset = document.getElementById('reset'),
rules = document.getElementById('rules'),
zones = document.getElementById('zones'),
spec = document.getElementById('spec'),
lastEvent = document.getElementById('lastEvent');
const NOMINAL = 10.000, SIGMA = 0.012, SHIFT_AT = 17, SHIFT = 0.55 * SIGMA;
let random = seeded(42),
subgroups = [];
function subgroup(index) {
const mean = NOMINAL + (index >= SHIFT_AT ? SHIFT : 0),
row = [];
for (let i = 0; i < 5; i++) {
row.push(Number((mean + gaussian(random) * SIGMA).toFixed(4)));
}
return row;
}
function load() {
random = seeded(42);
subgroups = [];
for (let i = 0; i < 25; i++) {
subgroups.push(subgroup(i));
}
xbar.limits = null;
xbar.baselineCount = 15;
xbar.specLimits = spec.checked ? { lsl: 9.960, usl: 10.040, target: 10.000 } : null;
/* A new array every time: the framework compares property values, and the array
the element already holds is deep-equal to itself however much it was mutated. */
xbar.data = subgroups.slice();
freeze.disabled = false;
}
load();
addSubgroup.addEventListener('click', function () {
subgroups.push(subgroup(subgroups.length));
xbar.data = subgroups.slice();
});
/* freezeLimits() holds whatever the chart shows now, which after the baseline is the
baseline - so this is the moment a qualification run ends and monitoring begins. */
freeze.addEventListener('click', function () {
const held = xbar.freezeLimits();
freeze.disabled = true;
lastEvent.textContent = held ?
'limits held at CL ' + held.center.toFixed(3) + ', UCL ' + held.ucl.toFixed(3) +
', LCL ' + held.lcl.toFixed(3) : '';
});
reset.addEventListener('click', function () {
load();
lastEvent.textContent = '';
});
rules.addEventListener('change', function () {
xbar.ruleSet = rules.value;
});
zones.addEventListener('change', function () {
xbar.showZones = zones.checked;
});
spec.addEventListener('change', function () {
xbar.specLimits = spec.checked ? { lsl: 9.960, usl: 10.040, target: 10.000 } : null;
});
/* The event fires when the set of offending points changes - not on every redraw. */
xbar.addEventListener('violation', function (event) {
lastEvent.textContent = event.detail.count ?
event.detail.count + ' point' + (event.detail.count === 1 ? '' : 's') + ' out of control' :
'in control';
});
/* ── Individuals and moving range ──────────────────────────────────── */
const imr = document.getElementById('imr'),
fills = [],
fillRandom = seeded(7);
for (let i = 0; i < 40; i++) {
/* A drift upward from the thirtieth reading - a worn dosing valve. */
const mean = 250 + (i >= 30 ? (i - 29) * 0.35 : 0);
fills.push(Number((mean + gaussian(fillRandom) * 1.2).toFixed(2)));
}
imr.baselineCount = 25;
imr.ruleSet = 'westernElectric';
imr.specLimits = { lsl: 246, usl: 254 };
imr.data = fills;
/* ── p chart ───────────────────────────────────────────────────────── */
const pchart = document.getElementById('pchart'),
sizes = [],
defectives = [],
pRandom = seeded(99);
for (let i = 0; i < 24; i++) {
/* Shifts inspect very different quantities, which is the whole point. */
const size = i % 3 === 0 ? 80 : (i % 3 === 1 ? 400 : 220),
rate = 0.035 + (i === 14 ? 0.06 : 0);
let count = 0;
for (let unit = 0; unit < size; unit++) {
if (pRandom() < rate) {
count++;
}
}
sizes.push(size);
defectives.push(count);
}
pchart.sampleSize = sizes;
pchart.data = defectives;
};
styles.css
smart-control-chart {
width: 100%;
}
#xbar {
--smart-control-chart-height: 420px;
}
#pchart {
--smart-control-chart-height: 300px;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.stats {
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.75;
}
.hint {
margin-top: 8px;
font-size: 12px;
opacity: 0.75;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in Angular, generated from the HTML demo. The frameworks guide has the project setup.
angular/app.component.html
<div class="angular-root" theme="industrial">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.ControlChart is a Shewhart control chart. It shows whether a process has changed, which is a different question from whether the process meets its specification.</h2>
<p>
Control limits are computed from the data: the centre line is the process average and the limits are three sigma on either side, estimated from the within-subgroup spread. Specification limits come from the customer and are drawn in their own style on the primary chart only; they are not used for the control limits or the run rules. All statistics are computed by <code>Smart.Industrial.spc</code>, so a capability figure in a report and the one under the chart are always the same.
</p>
</section>
<section>
<h2>X-bar and R chart: shaft diameter in subgroups of five</h2>
<p>
The limits are established from the first fifteen subgroups and then held. Around subgroup eighteen the process shifts by about half a sigma, which is not enough to put a point outside the limits. Press <em>Add subgroup</em> to continue sampling the shifted process and see the run rules detect the shift before any point is out of control.
</p>
<smart-control-chart id="xbar" type="xbar-r" label="Shaft diameter" unit="mm" [precisionDigits]="3"></smart-control-chart>
<div class="demo-actions">
<button type="button" id="addSubgroup">Add subgroup</button>
<button type="button" id="freeze">Freeze limits</button>
<button type="button" id="reset">Reset</button>
<label>Rules
<select id="rules">
<option value="nelson" selected>Nelson</option>
<option value="westernElectric">Western Electric</option>
<option value="both">Both</option>
<option value="none">None</option>
</select>
</label>
<label><input type="checkbox" id="zones" checked /> Zones</label>
<label><input type="checkbox" id="spec" checked /> Spec limits</label>
<span id="lastEvent" class="stats"></span>
</div>
<p class="hint">
The points that violate a rule and the rule names are listed as text under the chart. The same list is available to screen readers.
</p>
</section>
<section>
<h2>Individuals and moving range chart: fill weight, one unit at a time</h2>
<p>
When a process is measured one unit at a time there are no subgroups, so sigma is estimated from the moving range of successive readings. The moving range chart has one point fewer than the individuals chart.
</p>
<smart-control-chart id="imr" type="i-mr" label="Fill weight" unit="g" [precisionDigits]="1"></smart-control-chart>
</section>
<section>
<h2>p chart: fraction defective with unequal sample sizes</h2>
<p>
A shift that inspects 80 units gives less information than one that inspects 400, so its control limits are wider. The component computes the limits for each sample size and draws them as steps.
</p>
<smart-control-chart id="pchart" type="p" label="Fraction defective" [precisionDigits]="3" [showZones]="false"></smart-control-chart>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { ControlChartModule } from 'smart-industrial/angular/controlchart';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - ControlChart in Angular.
*
* The template is the HTML demo's markup: the elements are the wrappers, so what is
* written on them is checked against their inputs. demo.ts drives them the way the
* plant's data would, once every element in the view has rendered.
*/
@Component({
selector: 'app-root',
standalone: true,
imports: [ControlChartModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent implements AfterViewInit, OnDestroy {
private unmounted = false;
ngAfterViewInit(): void {
const root = document.querySelector('.angular-root') as HTMLElement,
pending: Promise<void>[] = [];
root.querySelectorAll('*').forEach((element: any) => {
if (typeof element.whenRendered === 'function' && !element.isRendered) {
pending.push(element.whenRendered());
}
});
Promise.all(pending).then(() => {
if (!this.unmounted) {
runDemo();
}
});
}
ngOnDestroy(): void {
this.unmounted = true;
}
}
angular/demo.ts
// @ts-nocheck
/* The HTML demo's script, as it was written: page code that feeds the elements, not a typed
API. The types are on the wrappers in app.component.ts. */
/* Every chart on this page is fed with data generated here, from a process with a known
mean and sigma, so what the chart reports can be checked against what was put in. The
seam is the `data` property - replace these generators with a query against your
measurement database and nothing else on the page changes. */
/* A seeded generator, so the page shows the same process on every load and the shift
lands where the text says it does. */
function seeded(seed) {
let state = seed >>> 0;
return function () {
state = (state * 1664525 + 1013904223) >>> 0;
return state / 4294967296;
};
}
/* A normal deviate from two uniforms. */
function gaussian(random) {
return Math.sqrt(-2 * Math.log(random() || 1e-12)) * Math.cos(2 * Math.PI * random());
}
export function runDemo(): void {
/* ── X-bar and R ────────────────────────────────────────────────────── */
const xbar = document.getElementById('xbar'),
addSubgroup = document.getElementById('addSubgroup'),
freeze = document.getElementById('freeze'),
reset = document.getElementById('reset'),
rules = document.getElementById('rules'),
zones = document.getElementById('zones'),
spec = document.getElementById('spec'),
lastEvent = document.getElementById('lastEvent');
const NOMINAL = 10.000, SIGMA = 0.012, SHIFT_AT = 17, SHIFT = 0.55 * SIGMA;
let random = seeded(42),
subgroups = [];
function subgroup(index) {
const mean = NOMINAL + (index >= SHIFT_AT ? SHIFT : 0),
row = [];
for (let i = 0; i < 5; i++) {
row.push(Number((mean + gaussian(random) * SIGMA).toFixed(4)));
}
return row;
}
function load() {
random = seeded(42);
subgroups = [];
for (let i = 0; i < 25; i++) {
subgroups.push(subgroup(i));
}
xbar.limits = null;
xbar.baselineCount = 15;
xbar.specLimits = spec.checked ? { lsl: 9.960, usl: 10.040, target: 10.000 } : null;
/* A new array every time: the framework compares property values, and the array
the element already holds is deep-equal to itself however much it was mutated. */
xbar.data = subgroups.slice();
freeze.disabled = false;
}
load();
addSubgroup.addEventListener('click', function () {
subgroups.push(subgroup(subgroups.length));
xbar.data = subgroups.slice();
});
/* freezeLimits() holds whatever the chart shows now, which after the baseline is the
baseline - so this is the moment a qualification run ends and monitoring begins. */
freeze.addEventListener('click', function () {
const held = xbar.freezeLimits();
freeze.disabled = true;
lastEvent.textContent = held ?
'limits held at CL ' + held.center.toFixed(3) + ', UCL ' + held.ucl.toFixed(3) +
', LCL ' + held.lcl.toFixed(3) : '';
});
reset.addEventListener('click', function () {
load();
lastEvent.textContent = '';
});
rules.addEventListener('change', function () {
xbar.ruleSet = rules.value;
});
zones.addEventListener('change', function () {
xbar.showZones = zones.checked;
});
spec.addEventListener('change', function () {
xbar.specLimits = spec.checked ? { lsl: 9.960, usl: 10.040, target: 10.000 } : null;
});
/* The event fires when the set of offending points changes - not on every redraw. */
xbar.addEventListener('violation', function (event) {
lastEvent.textContent = event.detail.count ?
event.detail.count + ' point' + (event.detail.count === 1 ? '' : 's') + ' out of control' :
'in control';
});
/* ── Individuals and moving range ──────────────────────────────────── */
const imr = document.getElementById('imr'),
fills = [],
fillRandom = seeded(7);
for (let i = 0; i < 40; i++) {
/* A drift upward from the thirtieth reading - a worn dosing valve. */
const mean = 250 + (i >= 30 ? (i - 29) * 0.35 : 0);
fills.push(Number((mean + gaussian(fillRandom) * 1.2).toFixed(2)));
}
imr.baselineCount = 25;
imr.ruleSet = 'westernElectric';
imr.specLimits = { lsl: 246, usl: 254 };
imr.data = fills;
/* ── p chart ───────────────────────────────────────────────────────── */
const pchart = document.getElementById('pchart'),
sizes = [],
defectives = [],
pRandom = seeded(99);
for (let i = 0; i < 24; i++) {
/* Shifts inspect very different quantities, which is the whole point. */
const size = i % 3 === 0 ? 80 : (i % 3 === 1 ? 400 : 220),
rate = 0.035 + (i === 14 ? 0.06 : 0);
let count = 0;
for (let unit = 0; unit < size; unit++) {
if (pRandom() < rate) {
count++;
}
}
sizes.push(size);
defectives.push(count);
}
pchart.sampleSize = sizes;
pchart.data = defectives;
}
angular/app.component.css
smart-control-chart {
width: 100%;
}
#xbar {
--smart-control-chart-height: 420px;
}
#pchart {
--smart-control-chart-height: 300px;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.stats {
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.75;
}
.hint {
margin-top: 8px;
font-size: 12px;
opacity: 0.75;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in React, generated from the HTML demo. The frameworks guide has the project setup.
react/App.jsx
import 'smart-industrial/source/styles/smart.default.css';
import 'smart-industrial/source/styles/smart.industrial-elements.css';
import 'smart-industrial/source/styles/smart.industrial.css';
import 'smart-industrial/source/styles/smart.industrial-dark.css';
import './App.css';
import React from 'react';
import { ControlChart } from 'smart-industrial/react/controlchart';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* Every chart on this page is fed with data generated here, from a process with a known
mean and sigma, so what the chart reports can be checked against what was put in. The
seam is the `data` property - replace these generators with a query against your
measurement database and nothing else on the page changes. */
/* A seeded generator, so the page shows the same process on every load and the shift
lands where the text says it does. */
function seeded(seed) {
let state = seed >>> 0;
return function () {
state = (state * 1664525 + 1013904223) >>> 0;
return state / 4294967296;
};
}
/* A normal deviate from two uniforms. */
function gaussian(random) {
return Math.sqrt(-2 * Math.log(random() || 1e-12)) * Math.cos(2 * Math.PI * random());
}
const App = () => {
const root = React.useRef(null);
React.useEffect(() => {
/* The elements render after React has mounted them, and the demo's logic - written
for a page's load event - reads their parts, so it waits for every element in the
tree to have rendered first. */
const pending = Array.from(root.current.querySelectorAll('*'))
.filter((element) => typeof element.whenRendered === 'function' && !element.isRendered)
.map((element) => element.whenRendered());
let unmounted = false;
Promise.all(pending).then(() => {
if (unmounted) {
return;
}
/* ── X-bar and R ────────────────────────────────────────────────────── */
const xbar = document.getElementById('xbar'),
addSubgroup = document.getElementById('addSubgroup'),
freeze = document.getElementById('freeze'),
reset = document.getElementById('reset'),
rules = document.getElementById('rules'),
zones = document.getElementById('zones'),
spec = document.getElementById('spec'),
lastEvent = document.getElementById('lastEvent');
const NOMINAL = 10.000, SIGMA = 0.012, SHIFT_AT = 17, SHIFT = 0.55 * SIGMA;
let random = seeded(42),
subgroups = [];
function subgroup(index) {
const mean = NOMINAL + (index >= SHIFT_AT ? SHIFT : 0),
row = [];
for (let i = 0; i < 5; i++) {
row.push(Number((mean + gaussian(random) * SIGMA).toFixed(4)));
}
return row;
}
function load() {
random = seeded(42);
subgroups = [];
for (let i = 0; i < 25; i++) {
subgroups.push(subgroup(i));
}
xbar.limits = null;
xbar.baselineCount = 15;
xbar.specLimits = spec.checked ? { lsl: 9.960, usl: 10.040, target: 10.000 } : null;
/* A new array every time: the framework compares property values, and the array
the element already holds is deep-equal to itself however much it was mutated. */
xbar.data = subgroups.slice();
freeze.disabled = false;
}
load();
addSubgroup.addEventListener('click', function () {
subgroups.push(subgroup(subgroups.length));
xbar.data = subgroups.slice();
});
/* freezeLimits() holds whatever the chart shows now, which after the baseline is the
baseline - so this is the moment a qualification run ends and monitoring begins. */
freeze.addEventListener('click', function () {
const held = xbar.freezeLimits();
freeze.disabled = true;
lastEvent.textContent = held ?
'limits held at CL ' + held.center.toFixed(3) + ', UCL ' + held.ucl.toFixed(3) +
', LCL ' + held.lcl.toFixed(3) : '';
});
reset.addEventListener('click', function () {
load();
lastEvent.textContent = '';
});
rules.addEventListener('change', function () {
xbar.ruleSet = rules.value;
});
zones.addEventListener('change', function () {
xbar.showZones = zones.checked;
});
spec.addEventListener('change', function () {
xbar.specLimits = spec.checked ? { lsl: 9.960, usl: 10.040, target: 10.000 } : null;
});
/* The event fires when the set of offending points changes - not on every redraw. */
xbar.addEventListener('violation', function (event) {
lastEvent.textContent = event.detail.count ?
event.detail.count + ' point' + (event.detail.count === 1 ? '' : 's') + ' out of control' :
'in control';
});
/* ── Individuals and moving range ──────────────────────────────────── */
const imr = document.getElementById('imr'),
fills = [],
fillRandom = seeded(7);
for (let i = 0; i < 40; i++) {
/* A drift upward from the thirtieth reading - a worn dosing valve. */
const mean = 250 + (i >= 30 ? (i - 29) * 0.35 : 0);
fills.push(Number((mean + gaussian(fillRandom) * 1.2).toFixed(2)));
}
imr.baselineCount = 25;
imr.ruleSet = 'westernElectric';
imr.specLimits = { lsl: 246, usl: 254 };
imr.data = fills;
/* ── p chart ───────────────────────────────────────────────────────── */
const pchart = document.getElementById('pchart'),
sizes = [],
defectives = [],
pRandom = seeded(99);
for (let i = 0; i < 24; i++) {
/* Shifts inspect very different quantities, which is the whole point. */
const size = i % 3 === 0 ? 80 : (i % 3 === 1 ? 400 : 220),
rate = 0.035 + (i === 14 ? 0.06 : 0);
let count = 0;
for (let unit = 0; unit < size; unit++) {
if (pRandom() < rate) {
count++;
}
}
sizes.push(size);
defectives.push(count);
}
pchart.sampleSize = sizes;
pchart.data = defectives;
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.ControlChart is a Shewhart control chart. It shows whether a process has changed, which is a different question from whether the process meets its specification.</h2>
<p>Control limits are computed from the data: the centre line is the process average and the limits are three sigma on either side, estimated from the within-subgroup spread. Specification limits come from the customer and are drawn in their own style on the primary chart only; they are not used for the control limits or the run rules. All statistics are computed by <code>Smart.Industrial.spc</code>, so a capability figure in a report and the one under the chart are always the same.</p>
</section>
<section>
<h2>X-bar and R chart: shaft diameter in subgroups of five</h2>
<p>The limits are established from the first fifteen subgroups and then held. Around subgroup eighteen the process shifts by about half a sigma, which is not enough to put a point outside the limits. Press <em>Add subgroup</em> to continue sampling the shifted process and see the run rules detect the shift before any point is out of control.</p>
<ControlChart id="xbar" type="xbar-r" label="Shaft diameter" unit="mm" precisionDigits={3}></ControlChart>
<div className="demo-actions">
<button type="button" id="addSubgroup">Add subgroup</button>
<button type="button" id="freeze">Freeze limits</button>
<button type="button" id="reset">Reset</button>
<label>
Rules
<select id="rules" defaultValue="nelson"><option value="nelson">Nelson</option> <option value="westernElectric">Western Electric</option> <option value="both">Both</option> <option value="none">None</option></select>
</label>
<label><input type="checkbox" id="zones" defaultChecked /> Zones</label>
<label><input type="checkbox" id="spec" defaultChecked /> Spec limits</label>
<span id="lastEvent" className="stats"></span>
</div>
<p className="hint">The points that violate a rule and the rule names are listed as text under the chart. The same list is available to screen readers.</p>
</section>
<section>
<h2>Individuals and moving range chart: fill weight, one unit at a time</h2>
<p>When a process is measured one unit at a time there are no subgroups, so sigma is estimated from the moving range of successive readings. The moving range chart has one point fewer than the individuals chart.</p>
<ControlChart id="imr" type="i-mr" label="Fill weight" unit="g" precisionDigits={1}></ControlChart>
</section>
<section>
<h2>p chart: fraction defective with unequal sample sizes</h2>
<p>A shift that inspects 80 units gives less information than one that inspects 400, so its control limits are wider. The component computes the limits for each sample size and draws them as steps.</p>
<ControlChart id="pchart" type="p" label="Fraction defective" precisionDigits={3} showZones={false}></ControlChart>
</section>
</div>
</div>
);
};
export default App;
react/App.css
smart-control-chart {
width: 100%;
}
#xbar {
--smart-control-chart-height: 420px;
}
#pchart {
--smart-control-chart-height: 300px;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.stats {
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.75;
}
.hint {
margin-top: 8px;
font-size: 12px;
opacity: 0.75;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in Vue, generated from the HTML demo. The frameworks guide has the project setup.
vue/App.vue
<template>
<div class="vue-root" theme="industrial" v-pre>
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.ControlChart is a Shewhart control chart. It shows whether a process has changed, which is a different question from whether the process meets its specification.</h2>
<p>
Control limits are computed from the data: the centre line is the process average and the limits are three sigma on either side, estimated from the within-subgroup spread. Specification limits come from the customer and are drawn in their own style on the primary chart only; they are not used for the control limits or the run rules. All statistics are computed by <code>Smart.Industrial.spc</code>, so a capability figure in a report and the one under the chart are always the same.
</p>
</section>
<section>
<h2>X-bar and R chart: shaft diameter in subgroups of five</h2>
<p>
The limits are established from the first fifteen subgroups and then held. Around subgroup eighteen the process shifts by about half a sigma, which is not enough to put a point outside the limits. Press <em>Add subgroup</em> to continue sampling the shifted process and see the run rules detect the shift before any point is out of control.
</p>
<smart-control-chart id="xbar" type="xbar-r" label="Shaft diameter" unit="mm" precision-digits="3"></smart-control-chart>
<div class="demo-actions">
<button type="button" id="addSubgroup">Add subgroup</button>
<button type="button" id="freeze">Freeze limits</button>
<button type="button" id="reset">Reset</button>
<label>Rules
<select id="rules">
<option value="nelson" selected>Nelson</option>
<option value="westernElectric">Western Electric</option>
<option value="both">Both</option>
<option value="none">None</option>
</select>
</label>
<label><input type="checkbox" id="zones" checked /> Zones</label>
<label><input type="checkbox" id="spec" checked /> Spec limits</label>
<span id="lastEvent" class="stats"></span>
</div>
<p class="hint">
The points that violate a rule and the rule names are listed as text under the chart. The same list is available to screen readers.
</p>
</section>
<section>
<h2>Individuals and moving range chart: fill weight, one unit at a time</h2>
<p>
When a process is measured one unit at a time there are no subgroups, so sigma is estimated from the moving range of successive readings. The moving range chart has one point fewer than the individuals chart.
</p>
<smart-control-chart id="imr" type="i-mr" label="Fill weight" unit="g" precision-digits="1"></smart-control-chart>
</section>
<section>
<h2>p chart: fraction defective with unequal sample sizes</h2>
<p>
A shift that inspects 80 units gives less information than one that inspects 400, so its control limits are wider. The component computes the limits for each sample size and draws them as steps.
</p>
<smart-control-chart id="pchart" type="p" label="Fraction defective" precision-digits="3" show-zones="false"></smart-control-chart>
</section>
</div>
</div>
</template>
<script>
import { onMounted } from "vue";
import "smart-industrial/source/styles/smart.default.css";
import "smart-industrial/source/styles/smart.industrial-elements.css";
import "smart-industrial/source/styles/smart.industrial.css";
import "smart-industrial/source/styles/smart.industrial-dark.css";
import "smart-industrial/source/modules/smart.controlchart.js";
/* Every chart on this page is fed with data generated here, from a process with a known
mean and sigma, so what the chart reports can be checked against what was put in. The
seam is the `data` property - replace these generators with a query against your
measurement database and nothing else on the page changes. */
/* A seeded generator, so the page shows the same process on every load and the shift
lands where the text says it does. */
function seeded(seed) {
let state = seed >>> 0;
return function () {
state = (state * 1664525 + 1013904223) >>> 0;
return state / 4294967296;
};
}
/* A normal deviate from two uniforms. */
function gaussian(random) {
return Math.sqrt(-2 * Math.log(random() || 1e-12)) * Math.cos(2 * Math.PI * random());
}
export default {
name: "app",
setup() {
onMounted(() => {
/* The elements render after Vue has mounted them, and the demo's logic - written for
a page's load event - reads their parts, so it waits for every element in the tree
to have rendered first. */
const pending = Array.from(document.querySelector(".vue-root").querySelectorAll("*"))
.filter((element) => typeof element.whenRendered === "function" && !element.isRendered)
.map((element) => element.whenRendered());
Promise.all(pending).then(() => {
/* ── X-bar and R ────────────────────────────────────────────────────── */
const xbar = document.getElementById('xbar'),
addSubgroup = document.getElementById('addSubgroup'),
freeze = document.getElementById('freeze'),
reset = document.getElementById('reset'),
rules = document.getElementById('rules'),
zones = document.getElementById('zones'),
spec = document.getElementById('spec'),
lastEvent = document.getElementById('lastEvent');
const NOMINAL = 10.000, SIGMA = 0.012, SHIFT_AT = 17, SHIFT = 0.55 * SIGMA;
let random = seeded(42),
subgroups = [];
function subgroup(index) {
const mean = NOMINAL + (index >= SHIFT_AT ? SHIFT : 0),
row = [];
for (let i = 0; i < 5; i++) {
row.push(Number((mean + gaussian(random) * SIGMA).toFixed(4)));
}
return row;
}
function load() {
random = seeded(42);
subgroups = [];
for (let i = 0; i < 25; i++) {
subgroups.push(subgroup(i));
}
xbar.limits = null;
xbar.baselineCount = 15;
xbar.specLimits = spec.checked ? { lsl: 9.960, usl: 10.040, target: 10.000 } : null;
/* A new array every time: the framework compares property values, and the array
the element already holds is deep-equal to itself however much it was mutated. */
xbar.data = subgroups.slice();
freeze.disabled = false;
}
load();
addSubgroup.addEventListener('click', function () {
subgroups.push(subgroup(subgroups.length));
xbar.data = subgroups.slice();
});
/* freezeLimits() holds whatever the chart shows now, which after the baseline is the
baseline - so this is the moment a qualification run ends and monitoring begins. */
freeze.addEventListener('click', function () {
const held = xbar.freezeLimits();
freeze.disabled = true;
lastEvent.textContent = held ?
'limits held at CL ' + held.center.toFixed(3) + ', UCL ' + held.ucl.toFixed(3) +
', LCL ' + held.lcl.toFixed(3) : '';
});
reset.addEventListener('click', function () {
load();
lastEvent.textContent = '';
});
rules.addEventListener('change', function () {
xbar.ruleSet = rules.value;
});
zones.addEventListener('change', function () {
xbar.showZones = zones.checked;
});
spec.addEventListener('change', function () {
xbar.specLimits = spec.checked ? { lsl: 9.960, usl: 10.040, target: 10.000 } : null;
});
/* The event fires when the set of offending points changes - not on every redraw. */
xbar.addEventListener('violation', function (event) {
lastEvent.textContent = event.detail.count ?
event.detail.count + ' point' + (event.detail.count === 1 ? '' : 's') + ' out of control' :
'in control';
});
/* ── Individuals and moving range ──────────────────────────────────── */
const imr = document.getElementById('imr'),
fills = [],
fillRandom = seeded(7);
for (let i = 0; i < 40; i++) {
/* A drift upward from the thirtieth reading - a worn dosing valve. */
const mean = 250 + (i >= 30 ? (i - 29) * 0.35 : 0);
fills.push(Number((mean + gaussian(fillRandom) * 1.2).toFixed(2)));
}
imr.baselineCount = 25;
imr.ruleSet = 'westernElectric';
imr.specLimits = { lsl: 246, usl: 254 };
imr.data = fills;
/* ── p chart ───────────────────────────────────────────────────────── */
const pchart = document.getElementById('pchart'),
sizes = [],
defectives = [],
pRandom = seeded(99);
for (let i = 0; i < 24; i++) {
/* Shifts inspect very different quantities, which is the whole point. */
const size = i % 3 === 0 ? 80 : (i % 3 === 1 ? 400 : 220),
rate = 0.035 + (i === 14 ? 0.06 : 0);
let count = 0;
for (let unit = 0; unit < size; unit++) {
if (pRandom() < rate) {
count++;
}
}
sizes.push(size);
defectives.push(count);
}
pchart.sampleSize = sizes;
pchart.data = defectives;
});
});
}
};
</script>
<style>
smart-control-chart {
width: 100%;
}
#xbar {
--smart-control-chart-height: 420px;
}
#pchart {
--smart-control-chart-height: 300px;
}
.demo-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
}
.stats {
font-size: 12px;
font-variant-numeric: tabular-nums;
opacity: 0.75;
}
.hint {
margin-top: 8px;
font-size: 12px;
opacity: 0.75;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
</style>
What the demo shows
- X-bar and R chart: shaft diameter in subgroups of five
- Individuals and moving range chart: fill weight, one unit at a time
- p chart: fraction defective with unequal sample sizes