Skip to content

SPC: the sampled chart drawn, and a click opens the five bottles behind the dot - #147

Merged
kalwei merged 7 commits into
mainfrom
feat/spc-xbar-chart-and-panel
Oct 6, 2026
Merged

kalwei merged 7 commits into
mainfrom
feat/spc-xbar-chart-and-panel

Conversation

@kalwei

@kalwei kalwei commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

What

Quality → SPC draws a sampled characteristic, and a click on a dot opens the
readings behind it.

Until now the screen drew one kind of control chart. A characteristic with a
sampling plan (#145) had its arithmetic, its samples and its verdict, and a
sentence saying the screen could not draw it. It draws it now:

  • X-bar and R. The mean of each sample above with X̿ and X̿ ± A2·R̄,
    the spread inside each sample below with R̄, D4·R̄ and D3·R̄, the
    specification limits where there are any, one dot per sample on each half.
    It is the IMR layout from SPC: the moving-range half of the chart, drawn — an IMR chart #144, not a third one: the same two halves in one
    <svg>, so one export takes both; the same selected-dot ring; the same
    promise that the chart does not move when the panel opens.
  • A click opens the n readings. On an individuals chart a point is a
    reading and the panel opens that reading. On a sampled chart a point is an
    average, so the panel opens the five bottles it is the average of — each
    one's value, gauge, who took it, how far it sat from the sample's own mean,
    and which of them is furthest out. That is the question an X-bar chart
    cannot answer on its own: five bottles high is a filler setting, one bottle
    high is a nozzle.
  • The window is the stretch those readings span, plus the minutes before,
    and every one of their stamps is marked on every trend — so which bottle was
    measured while the nozzle pressure was falling is visible rather than
    inferred. Everything else is Click a point on the SPC chart and see why it is there — no model, just the records #143's blocks asked of a sample: where it sat on
    both halves and what fired, the gauge and its calibration, the machine's
    state and what it had just come out of, the stops, maintenance, findings, and
    each block's own coverage.
  • GET /quality/spc/{material}/{characteristic}/sample/{sample_id} is the same
    read as an API, with the same before_minutes / after_minutes /
    neighbour_hours as the point read. Nothing in it is computed for the panel.

One envelope key was added, as the brief allows: a top-level range_chart
block on the chart read, beside moving_range — centre, upper, lower,
n, stable, verdict, flagged — so the lower half's words come from the
same place whichever kind of chart it is, with or without limits. It carries no
signals of its own: on a sampled chart rule 5 judges a sample and already
arrives in signals indexed by the same samples as rules 1–4, and a second
copy would have a reader counting every range firing twice. control.range_chart
(the #145 contract) is unchanged, key for key.

And a caption bug the lab found. On 2026-10-06 the lab's bottling plant,
with no samples yet, drew fill_height under the heading INDIVIDUALS AND
MOVING RANGE — 0 SAMPLES
with the individuals legend and the individuals
explainer beneath it. The heading, the legend and both sentences now follow
kind whether or not there is anything to draw. Decision 0040 made the chart
type a property of the plan; a caption that does not follow it undoes that.

Why

Scott, 2026-10-06: "test if this type of graphing and functionality can apply
to a new type of SPC."
It applies — the layout, the dot behaviour and the
panel carried over without a second design, and the one thing that genuinely
differs (a point is an average, so there are n readings and a window rather
than an instant) is two blocks and no new page.

How verified

  • pytest: full suite green. 33 new unit tests for the sample dossier
    (test_a_sample_on_the_control_chart_carries_the_five_bottles_behind_it.py),
    the chart half of the sampled-screen tests rewritten, and 25 Playwright
    tests
    in Click a point on the SPC chart and see why it is there — no model, just the records #143's style
    (test_clicking_a_sample_on_the_spc_chart_opens_the_five_bottles.py): the
    dossier fetch held back 1.2 s through a route intercept, every assertion
    waiting on the panel's own data-sample — which the page writes last —
    rather than on the box that will hold it; four themes with the palette
    probe; an operator and a supervisor both open it. They key on identity: one
    dot is flagged and it is the sample after the changeover, the bottle that
    pulled its average is the 147.0 mm one.
  • A scratch bottling plant from the lab pack at 60×, on 127.0.0.1:8765 /
    OPC 4871, stopped afterwards through fsmes plant … stop; the lab fleet's
    8090/8110/8120/4941/4942 and the demo's 9030/5433 were listening before and
    after. Sixteen samples of five bottles, limits appearing on their own at
    spc_min_points, X̿ 141.71, UCL 143.46, R̄ 3.04, D4·R̄ 6.42, verdict
    in control but not capable (Cpk 0.69). Clicked; the panel opened on the
    five readings with the furthest marked; added a four-at-a-time characteristic
    with no readings at all and the heading, legend and both sentences were the
    sampled ones.

Quality → SPC on fill height, with the panel open on one sample

(The image is in the diff at docs/plant/images/spc-xbar-and-r.png; the figures
above are the same run.)

The acceptance table from #143, extended

The bottling pack plants one fill-height story: the nozzle the changeover left
behind
— for twenty-five line minutes after each changeover the sample means
sit about half a millimetre high while the ranges stay where they were.

Cause Where it lands Clicks
the nozzle the changeover left behind — is the sample findable? No rule fires on it. Every post-changeover sample in the run sat at 141.90–142.22 and every other sample at 141.26–141.80 — six and nine, not one overlapping — but the control limits are X̿ ± 1.75 mm, because R̄ is 3.04 mm. A planted 0.5 mm shift against a 3 mm within-sample spread is a sixth of the limit width. A reader sees the saw-tooth by eye; nothing on the screen points at it by eye, then 1
…and once the sample is open, is the changeover named? No. The sample is taken at QI01, the inspection bench; the changeover is at FILL01. The machine block is the sample's own station's, so it shows the bench going idle between inspections, and the three trends are CameraLux, InspectMs, RejectPct. The filler is one machine upstream never, today
the five readings, and which one pulled the average first block, every time: value, distance from the sample's own mean, the furthest marked, with the sentence that one of them always is 1
whether it was the spread rather than the process the lower half's own limit and verdict are in the rules block beside the upper half's 1
the gauge HEIGHT-FILL-01, calibrated 2026-09-24, due 2027-03-23, resolution 0.1 mm against a 6 mm tolerance 1

So: not in one click, and the reason is the same one #143 found with the
washer.
The panel answers what happened at this station, and the planted
fill-height cause is a station upstream — the link to the line's other stations
over the same window is on the board as its own item, and this PR does not
build it. The second finding is about the pack rather than the product: a
simulated bottle-to-bottle spread six times the shift it plants makes the
story invisible to any control chart, including a correct one.

Honest, about the screen

The panel is 4,114 px tall on a sample. That is not a regression — the same
plant's point panel is 4,140 px — but on a sample the first screenful is
now the readings table, so what the rules said starts below the fold. The
block that should be collapsed first is this station's process values
(1,122 px). Collapsing the lower blocks is the separate waiting item and is
deliberately not in here; it is needed.

Not in here

No model. No new tab, no new table, no migration. The upstream-station link and
the collapsing panel are both on the board. Count charts and quality-team limit
editing are out of scope. control.range_chart, the individuals chart and the
point dossier are unchanged.

The red cell on this PR, and the seventh commit that fixes it

Everything passed after the rebase except test (windows-latest, 3.12), and
the two attempts failed on two different tests, neither in a file this
branch touches. Both are now fixed here, because both block a strict main and
a third re-run would only have found a third one.

tests/test_the_simulated_floor_plants_causes_an_engineer_can_find.py::test_the_heights_after_a_changeover_sit_half_a_millimetre_high
asked one sample of five either side of a changeover to see the pack's 0.5 mm
offset: the planted lift measured 0.06 mm against a tolerance of 0.35 mm.
Five bottles held at one weight still stand about 0.36 mm apart, so the
standard error of that difference is about 0.23 mm — the assertion was roughly
one standard error wide. It is worse at night, where the pack widens the
bench's spread from 0.10 mm to 0.25 mm, which is why it failed repeatably:
checked out and run here it failed 12 times out of 12 on main at 3bdc0886
and on f86ced15 before it, because the clock made it a night shift. It now
tests what the pack actually promises, in the pack's own words — "a shift a
reader finds by splitting the means on the changeover, not one that trips a
control rule"
— over the run: twelve samples a side, and the tolerance is
four standard errors of the difference worked out from the pack's own spreads
at the wider of the two shifts rather than a number chosen by hand. The shift
it is looking for is six standard errors out, so an offset that went missing
is still caught. Over ten seeds the answer landed 0.39–0.62 mm, at most 1.5
standard errors from 0.5.

tests/test_read_never_waits.py::test_a_login_answers_while_the_floor_screen_is_computing
asserted a one-second wall clock (1.06 s on main's own ubuntu-latest, 3.13
in run 37497028434; 1.41 s here on windows-latest, 3.12). I took the first
of the two options: the sign-in is now measured against the screen's own
measured time
, not the clock — it has to answer before the screen is half
way through. That comparison only became true once the screen was genuinely
slow, and making it genuinely slow turned up the real bug in the test: the
refresh it was supposedly racing was never signed in
, so it was a 401 that
opened no transaction and held nothing. The test has been measuring a sign-in
with nothing behind it since #80. Both clients now sign in, and the screen is
held open inside its own read transaction by a time.sleep, which hands the
GIL back — the other reason the old number drifted, since TestClient runs
the app in this process and a refresh that computes in Python starves the
sign-in thread regardless of any lock. The test asserts separately that the
screen really was held open, so the comparison has teeth.

Tests only in that commit; no behaviour changes, so no CHANGELOG entry.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Thanks for the first pull request. Checks: every commit signed off (git commit -s), ruff and pytest green, and the house-rules checklist in the template. The maintainer reviews within a few days.

kalwei added 6 commits October 6, 2026 12:05
…point

The SPC screen drew individuals only: a characteristic inspected five at a
time got a sentence saying so and no picture. It now draws both kinds from
one layout, chosen by the envelope's `kind` (decision 0040), and a click on
a sample opens the five readings behind it.

- `services/spc.py`: a `range_chart` block on a sampled chart, in the same
  place and the same shape as `moving_range` is on an individuals one -
  points, centre, limits, `stable` and its own verdict sentence. It carries
  no `signals` key and says why: rule 5 on a sampled chart judges one
  sample, so it is indexed by the same samples as rules 1 to 4 and arrives
  in `signals` beside them. A second copy would report every firing twice.
- `services/spc_point.py`: `sample_dossier`, the same six questions asked
  of a sample. The n readings with each one's distance from the sample mean
  and the furthest named; both halves of the chart; the gauge; the machine;
  the telemetry over the window the n readings span plus the minutes before,
  with every one of them marked. Four helpers now take a value rather than a
  check so one set of blocks serves both panels.
- `GET /quality/spc/{material}/{characteristic}/sample/{sample_id}`.
- `web/spc.js`: one `draw` for both kinds; the R half under the X-bar half
  on the same x scale and in the same <svg>, so one export takes both; dots
  carrying `data-sample`; the panel opening on a sample with `data-sample`
  written last. The heading, legend, explainer, hint, panel title and idle
  text follow `kind` with or without points. The characteristic list says
  how many pieces at a time each one is inspected.

Signed-off-by: Scott Kalwei <scottkalwei@gmail.com>
Signed-off-by: scottkalw <scottkalwei@gmail.com>
The five tests that asserted the screen cannot draw an X-bar and R chart are
now the tests that it draws one: fifteen dots on each half of one <svg>, the
same sample under the same x, X̿ above and R̄ below, a verdict for each half,
and a button that says what it opens. Two are new: one holds every word
around the chart that is about which chart it is, and one holds those same
words on a sampled characteristic with no samples yet - the state the lab's
bottling plant was in on 2026-10-06 when it drew a fill-height chart under
the heading INDIVIDUALS AND MOVING RANGE.

And a file for the sample dossier, keyed on identity throughout: which sample
is flagged, which bottle is furthest from its sample's mean, which stop the
machine had just come out of. The ones worth naming:

- the furthest bottle is the 147.0 mm one and the other four are not; five
  equal readings name none; two tied name both.
- rule 1 fires about the average and the range chart stays quiet, and a
  sample spread wider than the rest is flagged on the lower half instead -
  both read from `spc.chart` rather than worked out a second time.
- the window is the four minutes the five readings span plus the ten before,
  and every one of the five is marked on every trend.
- a sample left off the chart says so rather than claiming no rule fired.

One fix fell out of writing them: the readings table said nothing when every
reading in a sample was identical, because its "none of them pulled it"
sentence was conditioned on a list that is empty in exactly that case.

Signed-off-by: Scott Kalwei <scottkalwei@gmail.com>
Signed-off-by: scottkalw <scottkalwei@gmail.com>
Playwright over the sampled chart in the style of #143's: the dossier
fetch held back a second and a bit through a route intercept, every
assertion waiting on the panel's own data-sample rather than on the box
that will hold it, and the plant underneath shaped like the bottling
floor - fill height five bottles at a time, a filler that came back from
a changeover running high, a stop nobody labelled, a nozzle pressure
that stops partway through the window.

Keyed on identity: one dot is flagged and it is the sample after the
changeover, and the bottle that pulled its average is the 147.0 mm one.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
The sampled chart's entry said the screen did not draw it yet; it does.
The control-chart page gains the paragraph and the screenshot, a section
on what a dot means when it is an average, and the sample read beside
the point read in the API block.

The design note in spc.js named a lab plant by its registry name, which
is the one thing src/ may not contain.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
Taken on a scratch bottling plant from the lab pack at 60x: sixteen
samples of five bottles, the means above with their limits and the
spread inside each sample below, and the panel open on one sample's
five readings.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
…hat the stylesheet shouts

Signed-off-by: scottkalw <scottkalwei@gmail.com>
@kalwei
kalwei force-pushed the feat/spc-xbar-chart-and-panel branch from eecb325 to 9263d76 Compare October 6, 2026 17:06
…ey are wrong

Both red cells on #147 were pre-existing tests that measure a small signal
against a noise floor of the same size. Neither is in a file this change
touches; both block a strict main, so they are fixed here rather than
re-run until they pass.

`test_the_heights_after_a_changeover_sit_half_a_millimetre_high` asked one
sample of five either side of a changeover to see the pack's 0.5 mm offset.
Five bottles at one weight stand 0.36 mm apart, so the standard error of
that difference is about 0.23 mm and the assertion's whole tolerance was
0.35 mm - and more at night, where the pack widens the bench's spread from
0.10 mm to 0.25 mm, which is why it failed repeatably on any run the clock
made a night shift. It now splits the means on the changeover over the run,
which is what the pack promises in its own words, with the tolerance worked
out from the pack's spreads rather than chosen: twelve samples a side and
four standard errors of the difference. The shift it looks for is six
standard errors out, so an offset that went missing is still caught.

`test_a_login_answers_while_the_floor_screen_is_computing` asserted a
one-second wall clock on a shared runner. Two things were wrong with the
number. The refresh it was supposedly racing was never signed in, so it was
a 401 that opened no transaction and held nothing - the test has been
measuring a sign-in with nothing behind it since #80. And `TestClient` runs
the app in this process, so a refresh that does compute starves the sign-in
through the GIL and the number tracks how slow the runner is. Both clients
now sign in, the screen is held open inside its own read transaction by a
`time.sleep` that hands the GIL back, and the sign-in is measured against
the screen's own time: it must answer before the screen is half way
through. On a slower runner both numbers move together.

Tests only; no behaviour changes.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
@kalwei
kalwei merged commit 0501454 into main Oct 6, 2026
16 checks passed
@kalwei
kalwei deleted the feat/spc-xbar-chart-and-panel branch October 6, 2026 19:30
kalwei added a commit that referenced this pull request Oct 7, 2026
… window

#143 and #147 both found the same hole in the same words: "the panel answers
what happened at this station, and the planted cause is a station upstream."
Four of the planted causes are findable at the station; the washer's stop and
the line's changeover are not, because they happened on a machine beside it.

So a `line` block on both dossiers: the other work units under the same
parent, each with its stops and its changeovers in this window, read from the
same timeline and split by the same `_stops` the station's own block uses.

Nothing is inferred. Two things are recorded and both are used: every work
unit has a parent, and a stretch of not running is an interval with a reason
on it. There is no upstream and no downstream in it - bottling's own routing
runs Inspect before Fill while the bottles come off the filler to the bench,
so a step number from the routing orders the block and claims nothing else,
and the block names the routing it read.

Bounded the way the stops table and the analogs already are: the stations with
something in the window are read in full to a ceiling, the rest are counted,
and the ones that recorded nothing come back by name with the sentence saying
that is a statement about records and not about the machine.

Signed-off-by: scottkalw <scottkalwei@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant