Lattice Grid Buy a licence

task guide

An SPC / Cpk control-chart dashboard

One tolerance, declared once as spec on a column, is what a capability figure and a control chart both read. Below is a live process running that declaration, what Cpk and the chart are actually telling you, and the complete file, reused from the manufacturing SPC tutorial's own working example.

The dashboard, live

A process that runs on target for a stable stretch, then drifts. The control limits are fixed on an early baseline, so the drift breaks a rule instead of being absorbed into limits that keep widening to fit whatever the process just did.

Building…
Loading a live grid…

Open the full demo, including the React and Angular versions, or keep reading for the complete standalone file below.

Reading Cpk and the control chart

Cp compares the tolerance's width to the process's spread: how much room the tolerance gives a process that is perfectly centred. Cpk is the harsher number, because it also accounts for where the process actually sits: a process can have plenty of spread to spare and still score a low Cpk if it is running off-centre, closer to one limit than the other. 1.33 is the figure this dashboard's tile marks as a usual floor; below it, the process is judged too close to the edge of its tolerance for comfort even though every reading so far might still be inside it.

The control chart is a different question: not "is the spread acceptable" but "has anything changed". Each point is one reading, plotted against limits fixed on a baseline set of readings taken while the process was known to be stable. A reading outside those limits, or a run of points breaking one of Nelson's rules (a trend, a shift, points hugging one side), is a signal the process itself has moved, which is exactly what the drift after the baseline triggers here. The moving-range chart asks a related but separate question: not whether the level has moved, but whether the process has become less consistent reading to reading. The capability chart puts the distribution of readings directly against the tolerance band, so a shift or a widening spread is visible as overlap with the limit rather than only as a number in a tile.

All four read the same declaration: one column, one spec of lower, upper and target. Nothing about the tolerance is repeated per chart or per statistic, so a tolerance that changes updates every figure and every chart from the one place it is declared.

The complete file

Loaded by script tag, no build step: the grid and the charts module from a CDN, a seeded generator standing in for a hundred and sixty readings off a line, the tolerance declared once on the bore column, and the tiles and charts that read it. This is the same file the manufacturing SPC tutorial's own run it live link opens; reused here rather than re-derived, so what runs is exactly what is shown.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.70.0/lattice-grid.min.css">
<script src="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.70.0/lattice-grid.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.70.0/modules/charts.min.js"></script>

<style>
  body { font-family: system-ui, sans-serif; margin: 0; padding: 16px; background: #f6f7f9; color: #1b2430; }
  .tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 12px; }
  .tile { background: #fff; border: 1px solid #e3e6ea; border-radius: 11px; padding: 12px 14px; min-width: 0; }
  #control { height: 240px; background: #fff; border: 1px solid #e3e6ea; border-radius: 10px; margin-bottom: 12px; }
  .pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
  .pair > div { height: 210px; background: #fff; border: 1px solid #e3e6ea; border-radius: 10px; min-width: 0; }
  #grid { height: 360px; background: #fff; border: 1px solid #e3e6ea; border-radius: 10px; overflow: hidden; }
  @media (max-width: 820px) { .tiles, .pair { grid-template-columns: 1fr; } }
</style>

<div class="tiles">
  <div class="tile" id="cpk"></div>
  <div class="tile" id="mean"></div>
  <div class="tile" id="defect"></div>
</div>
<div id="control"></div>
<div class="pair"><div id="mr"></div><div id="cap"></div></div>
<div id="grid"></div>

<script>
// A seeded generator standing in for readings off the line: one bore diameter
// per part, in millimetres. The process runs on target for a stable stretch,
// then drifts up, so the study has both a healthy run and a real fault to catch.
function rng(seed) {
  var a = seed >>> 0;
  return function () {
    a = (a + 0x6d2b79f5) >>> 0;
    var t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

// Box-Muller, so the readings are normally distributed around the target.
function gauss(rand) {
  return Math.sqrt(-2 * Math.log(Math.max(rand(), 1e-9))) * Math.cos(2 * Math.PI * rand());
}

function generateReadings(count) {
  var rand = rng(907);
  var MACHINES = ['Cell A', 'Cell B', 'Cell C'];
  var rows = [];
  for (var i = 0; i < count; i++) {
    // Stable for the first eighty parts, then a slow upward drift.
    var drift = i < 80 ? 0 : (i - 80) * 0.0011;
    var bore = Math.round((10.0 + drift + gauss(rand) * 0.013) * 1000) / 1000;
    rows.push({ id: i + 1, n: i + 1, machine: MACHINES[i % 3], bore: bore });
  }
  return rows;
}

// The tolerance lives on the column as spec: the target and the two limits the
// customer will accept. Cp and Cpk read it, the control chart draws it apart
// from the process's own limits, and the out-of-spec formatting below uses it.
var SPEC = { lower: 9.95, upper: 10.05, target: 10 };

var columns = [
  { field: 'n', title: '#', type: 'number', layout: { width: 80, pin: 'start' } },
  { field: 'machine', title: 'Machine', filter: { type: 'set' }, layout: { width: 120 } },
  {
    field: 'bore', title: 'Bore', type: 'number',
    format: { suffix: ' mm', decimals: 3 },
    spec: SPEC,
  },
  // A rolling mean over the last eight parts, computed in one ordered pass. The
  // leading parts have no full window and read blank rather than pretending.
  {
    id: 'trend', title: 'Rolling mean', type: 'number', format: { suffix: ' mm', decimals: 3 },
    shadow: { kind: 'rollingAvg', of: 'bore', orderBy: 'n', window: { kind: 'count', span: 8 } },
  },
];

var grid = LatticeGrid.createGrid(document.getElementById('grid'), {
  rowKey: 'id',
  theme: 'light',
  rows: generateReadings(160),
  columns: columns,
  selection: 'multiple',
  statusBar: true,
  // Out-of-spec conditional formatting: any bore outside the tolerance is marked
  // in red, so a scrapped part is visible in the table as well as on the chart.
  formatting: {
    bore: [
      { id: 'below', when: { op: 'lt', value: SPEC.lower }, style: { color: '#c92a2a', fontWeight: '600' } },
      { id: 'above', when: { op: 'gt', value: SPEC.upper }, style: { color: '#c92a2a', fontWeight: '600' } },
    ],
  },
  // The statistics panel, docked and open, so the figures are the first thing
  // seen and follow the filters.
  toolPanel: {
    side: 'left',
    panels: ['columns', 'filters', 'statistics'],
    openPanel: 'statistics',
    actions: ['excel', 'restore'],
    exportName: 'readings',
  },
});

// Three tiles read from the capability report. baseline fixes the limits on the
// first thirty parts, so the drift is measured against a healthy process rather
// than absorbed into a widening band. Each tile follows the filters.
var cap = function () { return grid.statistics.capability('bore', { rules: 'nelson', baseline: 30 }); };

LatticeGrid.createStat({ grid: grid, container: document.getElementById('cpk'), title: 'Cpk', value: function () { return cap().cpk; }, goodWhen: 'up', baseline: 1.33, decimals: 2, footer: '1.33 is the usual floor' });
LatticeGrid.createStat({ grid: grid, container: document.getElementById('mean'), title: 'Mean bore', of: 'bore', fn: 'avg', decimals: 3 });
LatticeGrid.createStat({ grid: grid, container: document.getElementById('defect'), title: 'Defect rate', value: function () { return cap().defectRate; }, goodWhen: 'down', format: { style: 'percent', decimals: 2 }, footer: 'share outside the tolerance' });

// The SPC study, all from the one declared tolerance. The individuals chart
// asks whether the process has moved; the moving-range chart asks whether it has
// become less repeatable; the capability chart draws the spread against the
// tolerance. Nelson's rules judge the points, and the limits are fixed on the
// first thirty parts.
var control = LatticeGrid.createChart({ grid: grid, container: '#control', type: 'control', y: 'bore', baseline: 30, rules: 'nelson', title: 'Individuals chart, limits fixed on the first 30 parts' });
var movingRange = LatticeGrid.createChart({ grid: grid, container: '#mr', type: 'movingRange', y: 'bore', title: 'Moving range' });
var capability = LatticeGrid.createChart({ grid: grid, container: '#cap', type: 'capability', y: 'bore', title: 'Capability against the tolerance' });
</script>

What next

For the out-of-spec conditional formatting, the rolling mean beside each reading, and pulling the exact readings that broke a Nelson rule, see the full manufacturing SPC dashboard tutorial, built along step by step from this same data. For statistics and shadow columns generally, see statistics and shadow columns. If the readings you would feed this already live in a pandas DataFrame, see edit a DataFrame in the browser.