Lattice Grid Buy a licence

demo D298

Manufacturing quality analysis

100,000 measurements from eight production lines: a process strip, a distribution, a control chart, two derived panels and the readings themselves, all recomputing from one line click

statistics.capability · mode: derived · createStat

A whole quality screen over 100,000 bore measurements from eight production lines: Cpk, mean, spread and the out-of-tolerance count, a distribution drawn against the tolerance, a control chart, a per-line summary, the readings that broke the limits, and the measurements themselves. Click Line 3 and every figure recomputes over that line alone, then click an exception and the grid scrolls to the record behind it.

Building…
Loading a live grid…

The configuration

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

<div id="tiles" style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px"></div>
<div id="distribution" style="height: 260px"></div>
<div id="control" style="height: 260px"></div>
<div id="by-line" style="height: 300px"></div>
<div id="exceptions" style="height: 300px"></div>
<div id="measurements" style="height: 380px"></div>

<script type="module">
  import { createChart } from '@toclocoinc/lattice-grid/modules/charts';

  const SPEC = { lower: 11.94, upper: 12.06, target: 12 };

  // The readings. The tolerance is declared once, on the column, so the tiles,
  // the distribution and the control chart all read the same three numbers.
  const measurements = LatticeGrid.createGrid(document.getElementById('measurements'), {
    rowKey: 'id',
    selection: { mode: 'single' },
    columnDefaults: { filter: true },
    columns: [
      { field: 'measurementId', title: 'Measurement' },
      { field: 'timestamp', title: 'Time', type: 'datetime' },
      { field: 'line', title: 'Line', filter: { type: 'set' } },
      { field: 'machine', title: 'Machine', filter: { type: 'set' } },
      { field: 'measurement', title: 'Bore', type: 'number',
        format: { decimals: 4, suffix: ' mm' }, spec: SPEC, total: 'median' },
      { field: 'deviation', title: 'Off target', type: 'number', format: { decimals: 4 } },
      { field: 'outOfSpec', title: 'Out of spec', type: 'number', layout: { hidden: true } },
    ],
    rows,
  });

  // One capability pass per state of the screen, shared by every tile: keyed on
  // what the grid is showing rather than invalidated by a listener, so it does
  // not matter which recomputes first.
  let token = '';
  let cap = null;
  const capability = () => {
    const now = measurements.rows.count() + '|' + JSON.stringify(measurements.filters.get() ?? null);
    if (now !== token) { token = now; cap = measurements.statistics.capability('measurement'); }
    return cap;
  };

  const tile = () => document.getElementById('tiles').appendChild(document.createElement('div'));

  // Each tile is banded in its own direction. bands judges the value; goodWhen
  // judges the change. A Cpk of 0.9 is bad news whichever way it got there.
  LatticeGrid.createStat({
    grid: measurements, container: tile(), title: 'Cpk',
    value: () => capability()?.cpk ?? null, decimals: 2,
    bands: { good: 1.33, warn: 1.0 },            // higher is better
    goodWhen: 'up', baseline: 1.33,
    interval: () => capability()?.interval ?? null,
  });
  LatticeGrid.createStat({
    grid: measurements, container: tile(), title: 'Mean bore',
    value: () => capability()?.mean ?? null, decimals: 4,
    goodWhen: 'neither', baseline: SPEC.target,  // close to target, not high or low
  });
  LatticeGrid.createStat({
    grid: measurements, container: tile(), title: 'Sigma',
    value: () => capability()?.sigmaOverall ?? null, decimals: 5,
    bands: { good: 0.015, warn: 0.02, direction: 'down' },  // lower is better
    goodWhen: 'down',
  });
  LatticeGrid.createStat({
    grid: measurements, container: tile(), title: 'Out of tolerance',
    value: () => capability()?.outOfSpec ?? null, decimals: 0,
    // A count cannot be banded on its own number when the row set changes size.
    // Band it on the rate it represents and the tile means one thing at every level.
    bands: (value) => {
      const total = measurements.rows.count();
      if (!total) return null;
      const rate = value / total;
      return rate <= 0.001 ? 'good' : rate <= 0.01 ? 'warn' : 'bad';
    },
    goodWhen: 'down',
  });

  // A derived grid: it reads whatever the measurements grid is filtered to,
  // groups it and reduces it, so a filter above rewrites this panel.
  const byLine = LatticeGrid.createGrid(document.getElementById('by-line'), {
    source: {
      mode: 'derived', from: measurements, follow: 'filtered', refresh: 'live',
      groupBy: 'line',
      select: {
        readings: { fn: 'count' },
        mean: { of: 'measurement', fn: 'avg' },
        sigma: { of: 'measurement', fn: 'stddev' },
        defects: { of: 'outOfSpec', fn: 'sum' },
      },
    },
    state: { sort: [{ col: 'ppk', dir: 'asc' }] },
    columns: [
      { field: 'line', title: 'Line' },
      { field: 'readings', title: 'Readings', type: 'number' },
      { field: 'mean', title: 'Mean', type: 'number', format: { decimals: 4 } },
      { field: 'sigma', title: 'Sigma', type: 'number', format: { decimals: 5 } },
      // Ppk is the panel's own mean and spread against the tolerance, which is
      // how the index is defined; it matches what capability() returns.
      { id: 'ppk', title: 'Ppk', type: 'number', format: { decimals: 2 },
        value: { deps: ['mean', 'sigma'], compute: (d) => d.sigma > 0
          ? Math.min((SPEC.upper - d.mean) / (3 * d.sigma), (d.mean - SPEC.lower) / (3 * d.sigma))
          : null } },
      { field: 'defects', title: 'Out of spec', type: 'number' },
    ],
  });

  // A second derived grid: the same rows, narrowed rather than grouped.
  const exceptions = LatticeGrid.createGrid(document.getElementById('exceptions'), {
    source: {
      mode: 'derived', from: measurements, follow: 'filtered', refresh: 'live',
      where: (r) => r.outOfSpec === 1,
      sort: [{ col: 'ts', dir: 'desc' }],
    },
    selection: { mode: 'single' },
    columns: [
      { field: 'timestamp', title: 'Time', type: 'datetime' },
      { field: 'machine', title: 'Machine' },
      { field: 'measurement', title: 'Bore', type: 'number', format: { decimals: 4, suffix: ' mm' } },
      { field: 'deviation', title: 'Off target', type: 'number', format: { decimals: 4 } },
    ],
  });

  // Insight to record: a derived row still carries the fields it was derived
  // from, so the key is enough to reach the reading in the grid underneath.
  // The event is row:clicked. A handler on row:click subscribes without error
  // and never fires.
  exceptions.on('row:clicked', (e) => {
    const id = e.row.data.id;
    measurements.selection.set([id]);
    measurements.scroll.toRow(id);
    measurements.highlight({ key: id }, { colour: '#fdeaea', duration: 0 });
  });

  // The distribution reads the measurements directly and redraws as they narrow.
  createChart({ grid: measurements, container: '#distribution', type: 'capability', y: 'measurement' });

  // An individuals control chart puts one reading on each point, so it is bound
  // to readings, not to a summary of them: take every stride-th reading in
  // production order rather than averaging them into buckets first.
  const sample = [];
  const stride = Math.max(1, Math.ceil(measurements.rows.count() / 240));
  for (let i = 0; i < measurements.rows.count(); i += stride) sample.push(measurements.rows.get(i).data);
  const readings = LatticeGrid.createHeadlessGrid({
    rowKey: 'id', rows: sample,
    columns: [{ field: 'measurement', type: 'number', spec: SPEC }],
  });
  createChart({ grid: readings, container: '#control', type: 'control', y: 'measurement', rules: 'nelson' });

  // Filter the readings and every panel above follows.
  document.getElementById('line-3').addEventListener('click', () => {
    measurements.filters.set({ col: 'line', op: 'eq', value: 'Line 3' });
  });
</script>

From a number you distrust to the record that explains it

Eight lines have been running for a week, and the plant-wide figures at the top are the ones a report would carry: a capability index, the average bore, the spread of the readings and how many of them fell outside the tolerance the customer set. They say something is not right. They cannot say what.

Everything on this screen reads the measurements grid at the bottom, so narrowing it narrows all of them together. Pick a line from the chips and the four figures recompute, the distribution redraws, the control chart redraws, the per-line summary re-derives and the exception list re-derives, over the readings that are left. Nothing is refetched and there is no second copy of the data anywhere on the page.

The Process by line panel is the one that answers the question. It is sorted worst first, so the line that needs attention is the top row. Its mean sits high, its spread is wider than every other line’s, its Cpk is the only one under the floor, and its Ppk, the index that judges the whole run rather than the moment, is lower still. That gap between the two is the finding: a process that is repeatable minute to minute and has been allowed to wander over the week.

What the click actually does

Click Line 3 and the whole screen becomes that one line. The distribution slides toward the upper tolerance instead of sitting centred on target. The control chart, which is now a genuine single-process chart rather than eight processes overlaid, climbs steadily from the centre line towards the upper limit. The out-of-tolerance count that looked small across the plant is revealed as almost entirely one line’s, and at that line’s own volume it is a rate an inspector would stop the line over.

Then click a row in Exceptions. The measurements grid scrolls to that reading and marks it, and the panel underneath opens the operational record: the machine, the operator, the batch, the shift, the bore and how far off target it landed, the tolerance it was judged against, and the temperature, pressure and speed the machine was running at when it was taken. That is the whole round trip in three clicks. A figure you distrust, the cause behind it, and the record you can act on.

Where each figure comes from

The four figures at the top are one capability pass over the rows in view, so they cannot disagree with each other or with the charts. Cpk and the spread are read from that pass, not assembled by hand. The Cpk tile carries its confidence interval underneath, because a capability index quoted without one is the commonest way a capability study overstates itself.

Each tile is banded in its own direction, which is not a detail. Higher Cpk is better, so its bands run upward from the 1.33 floor. Lower spread is better, so its bands run the other way, with cut points taken from the tolerance itself: a centred process holds Cpk 1.33 up to 0.015 mm of spread and Cpk 1.0 up to 0.020. Fewer out-of-tolerance readings is better, and because the count is graded on the rate it represents rather than on the raw number, the tile means the same thing whether eight lines are in view or one. The mean carries no band at all, because a mean is not better high or low, it is better close to target, and colouring it either way would be a claim the number does not support.

The Process by line panel is a derived grid: it groups whatever the measurements grid is filtered to and reduces it, so a filter above rewrites the panel with no second query. Its Ppk column is computed from the panel’s own mean and spread, which is exactly how the index is defined, and it matches the number the statistics engine returns for the same rows.

Why the control chart plots readings rather than averages

An individuals chart puts one reading on each point and estimates short-term variation from the gaps between consecutive readings. Rolling the readings into hourly averages first and charting those would look tidier and would be a different instrument with different limits, so the chart takes a sample of the readings themselves, every stride-th one in production order, and says in its title exactly which ones it drew.

How do I build a screen where filtering one grid recomputes everything?

Put the readings in a grid, declare the tolerance on the column with spec: { lower, upper, target }, and let every panel above read that grid. createStat tiles bound to it recompute on every filter change. A source: { mode: 'derived', from: measurements, follow: 'filtered', groupBy: 'line', select: { … } } grid re-derives from whatever is left. grid.statistics.capability('measurement') returns Cp, Cpk, Pp, Ppk, both estimates of sigma, the out-of-tolerance count and the rule violations over the rows in view. Because the tolerance is declared once on the column, the tiles, the distribution and the control chart cannot disagree about what the limits are.