@wafertools/wafermap 0.30.0 → 0.30.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +68 -9
- package/CHANGELOG.md +247 -0
- package/README.md +3 -3
- package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
- package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
- package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
- package/dist/packages/canvas-adapter/charts/capability.js +1 -1
- package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
- package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +1 -0
- package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
- package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
- package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
- package/dist/packages/canvas-adapter/charts/trend.js +1 -1
- package/dist/packages/canvas-adapter/chunked.d.ts +45 -0
- package/dist/packages/canvas-adapter/chunked.js +1 -0
- package/dist/packages/canvas-adapter/deprecated.d.ts +0 -3
- package/dist/packages/canvas-adapter/deprecated.js +1 -1
- package/dist/packages/canvas-adapter/dieList.js +1 -1
- package/dist/packages/canvas-adapter/exportName.d.ts +68 -0
- package/dist/packages/canvas-adapter/exportName.js +1 -0
- package/dist/packages/canvas-adapter/icons.js +2 -2
- package/dist/packages/canvas-adapter/index.d.ts +2 -1
- package/dist/packages/canvas-adapter/index.js +1 -1
- package/dist/packages/canvas-adapter/insightsTab.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +80 -3
- package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +24 -2
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +76 -15
- package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
- package/dist/packages/canvas-adapter/toCanvas.js +1 -1
- package/dist/packages/canvas-adapter/toolbar.d.ts +6 -0
- package/dist/packages/canvas-adapter/toolbar.js +2 -2
- package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
- package/dist/packages/canvas-adapter/userGuideHtml.js +15 -8
- package/dist/packages/canvas-adapter/version.d.ts +2 -2
- package/dist/packages/canvas-adapter/version.js +1 -1
- package/dist/packages/canvas-adapter/warnings.js +1 -1
- package/dist/packages/core/aggregates.js +1 -1
- package/dist/packages/core/deprecated.d.ts +8 -12
- package/dist/packages/core/deprecated.js +1 -1
- package/dist/packages/core/index.d.ts +1 -1
- package/dist/packages/core/index.js +1 -1
- package/dist/packages/core/utils.d.ts +67 -0
- package/dist/packages/core/utils.js +1 -1
- package/dist/packages/core/waferLabel.d.ts +12 -2
- package/dist/packages/core/waferLabel.js +1 -1
- package/dist/packages/renderer/binColors.d.ts +52 -0
- package/dist/packages/renderer/binColors.js +1 -1
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.d.ts +30 -5
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/renderer/deprecate.d.ts +7 -0
- package/dist/packages/renderer/deprecate.js +1 -1
- package/dist/packages/renderer/deprecated.d.ts +2 -2
- package/dist/packages/renderer/deprecated.js +1 -1
- package/dist/packages/renderer/index.d.ts +2 -1
- package/dist/packages/renderer/index.js +1 -1
- package/dist/packages/stats/analyzeWaferLot.js +1 -1
- package/dist/packages/stats/analyzeWaferMap.js +1 -1
- package/dist/packages/stats/capability.d.ts +72 -1
- package/dist/packages/stats/capability.js +1 -1
- package/dist/packages/stats/correlation.d.ts +30 -0
- package/dist/packages/stats/correlation.js +1 -1
- package/dist/packages/stats/deprecated.d.ts +30 -35
- package/dist/packages/stats/deprecated.js +1 -1
- package/dist/packages/stats/findingsNarrative.js +1 -1
- package/dist/packages/stats/index.d.ts +4 -2
- package/dist/packages/stats/index.js +1 -1
- package/dist/packages/stats/math.d.ts +27 -2
- package/dist/packages/stats/math.js +1 -1
- package/dist/packages/stats/normalizeInput.d.ts +10 -0
- package/dist/packages/stats/normalizeInput.js +1 -0
- package/dist/packages/stats/regions.d.ts +2 -0
- package/dist/packages/stats/regions.js +1 -1
- package/dist/packages/stats/renderFindingsReport.js +1 -1
- package/dist/packages/stats/renderSummaryReport.d.ts +39 -1
- package/dist/packages/stats/renderSummaryReport.js +27 -27
- package/dist/packages/stats/reportHtml.d.ts +0 -1
- package/dist/packages/stats/reportHtml.js +10 -10
- package/dist/packages/stats/summaryFigures.d.ts +47 -0
- package/dist/packages/stats/summaryFigures.js +1 -0
- package/dist/packages/stats/types.d.ts +118 -0
- package/llms.txt +12 -7
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -25,8 +25,24 @@ loudly over guessing.
|
|
|
25
25
|
- `@wafertools/wafermap/worker` — `createWafermapWorker()` for off-main-thread builds.
|
|
26
26
|
|
|
27
27
|
Default path: `buildWaferMap()` once when data loads, then `renderWaferMap()` for a
|
|
28
|
-
single wafer or `renderWaferGallery()` for several.
|
|
29
|
-
|
|
28
|
+
single wafer or `renderWaferGallery()` for several.
|
|
29
|
+
|
|
30
|
+
**The types still export a large deprecated surface that is removed in 0.31.0 — do not
|
|
31
|
+
reach into it just because autocomplete offers it.** Four groups, all replaced by the
|
|
32
|
+
default path above:
|
|
33
|
+
|
|
34
|
+
- the low-level drawing pipeline — `buildView()`, `toCanvas()`, `createWafer()`,
|
|
35
|
+
`generateDies()`, and the geometry and transform helpers around them;
|
|
36
|
+
- the chart-data builders — `buildYieldData()`, `buildCorrelationMatrix()`,
|
|
37
|
+
`buildTestBoxplotData()` and the rest: these were the internals of the Insights tab,
|
|
38
|
+
which the renderers now mount for you (see below);
|
|
39
|
+
- the region builders — `buildRingRegions()`, `buildQuadrantRegions()` and friends:
|
|
40
|
+
region yield comes back from `analyzeWaferMap()`;
|
|
41
|
+
- per-die and per-colour helpers — `getDieTestValue()`, `buildHoverText()`,
|
|
42
|
+
`resolveBinColors()`, `getValueColorScheme()`, `valueToViridis()`.
|
|
43
|
+
|
|
44
|
+
If the only way to do something is through one of these, that is a library gap worth
|
|
45
|
+
reporting, not a pattern to build on.
|
|
30
46
|
|
|
31
47
|
### Traps that produce silently wrong maps
|
|
32
48
|
|
|
@@ -37,8 +53,20 @@ or the other low-level pipeline functions: they are deprecated and removed in 0.
|
|
|
37
53
|
through unchanged; `dieConfig.width`/`height` convert to physical units. Do not
|
|
38
54
|
pre-multiply. The geometry inputs are `waferConfig` (type `WaferConfig`) and
|
|
39
55
|
`dieConfig` (type `DieConfig`) — both optional, both inferred when omitted.
|
|
40
|
-
-
|
|
41
|
-
|
|
56
|
+
- **Bins and test values must be numbers, and verdicts booleans.** Every parser —
|
|
57
|
+
CSV, JSON, a spreadsheet export — hands you `"1"`, and `"1"` is not pass bin 1: those
|
|
58
|
+
dies count as fails and the yield is wrong, while a test value left as text is not
|
|
59
|
+
plotted or analysed correctly. Convert with `Number()` at parse time. `buildWaferMap`
|
|
60
|
+
samples the input and reports `input-values-not-numbers` (severity `'error'`) rather
|
|
61
|
+
than coercing behind your back, so a build that "works" can still be wrong — read the
|
|
62
|
+
warnings.
|
|
63
|
+
- **`passBins` and `ringCount` are set once, on `buildWaferMap`, and travel on the
|
|
64
|
+
result.** Neither is an option on `analyzeWaferMap`, `analyzeWaferLot`,
|
|
65
|
+
`renderWaferMap` or `renderWaferGallery` — passing one there is a type error in
|
|
66
|
+
TypeScript, and in JavaScript it is ignored with an `analysis-option-corrected`
|
|
67
|
+
warning while the real value is read from the map. `passBins` decides both the yield
|
|
68
|
+
number and the wording of its label, so set it from the actual test program: do not
|
|
69
|
+
assume `[1]`, and never re-default to `[1]` downstream — read `result.passBins`.
|
|
42
70
|
- **`testValues` is keyed by test number**, e.g. `{ 1050: 0.42 }` — not a positional
|
|
43
71
|
array. `activeTest` likewise takes a *test number* (`1050`), not an index.
|
|
44
72
|
- **Functional tests (`testType: 'F'`) have no measured value.** Read their verdicts
|
|
@@ -84,9 +112,30 @@ or the other low-level pipeline functions: they are deprecated and removed in 0.
|
|
|
84
112
|
and its pass/fail verdict (pass bins, per `passBins`, take green pass colours), so a
|
|
85
113
|
bin is the same colour in every lot. To choose colours, set `BinDef.color` or register
|
|
86
114
|
a palette with `registerBinColorScheme`. Bin maps and value maps have separate
|
|
87
|
-
schemes: `binColorScheme` and `valueColorScheme`.
|
|
115
|
+
schemes: `binColorScheme` and `valueColorScheme`. Need the colours for a surface of
|
|
116
|
+
your own (a table swatch, an export)? Read `controller.getBinColors()` for a live map,
|
|
117
|
+
or `binColorsForMaps(results)` — never a palette lookup of your own.
|
|
88
118
|
- Build once, render many: `buildWaferMap()` handles data + geometry; re-render UI
|
|
89
|
-
changes through the controller's `setOptions()`, not by rebuilding.
|
|
119
|
+
changes through the controller's `setOptions()`, not by rebuilding. New data for a
|
|
120
|
+
map that is already mounted goes through `setResult()` — do not `destroy()` and
|
|
121
|
+
remount.
|
|
122
|
+
- **The analysis surfaces are already built — do not reimplement them.** Pass
|
|
123
|
+
`statsSummary` to `renderWaferMap` and it mounts the Summary panel; pass
|
|
124
|
+
`insights: { enabled: true }` and it mounts the chart suite (yield, bin pareto,
|
|
125
|
+
boxplot, histogram, correlation, scatter, capability). `renderWaferGallery` takes the
|
|
126
|
+
same option across a whole lot. Supply or replace the analysis later with
|
|
127
|
+
`setStatsSummary()`. Hand-building those charts is what the deprecated chart-data
|
|
128
|
+
builders were for, and they go in 0.31.0.
|
|
129
|
+
- **Click-to-highlight is wired, not hand-rolled.** `onSelect` reports what the user
|
|
130
|
+
picked; `setSelection(dies)` / `clearSelection()` drive it from your own UI — for
|
|
131
|
+
example from a finding, whose `dieKeys` match `getDieKey(die)` exactly.
|
|
132
|
+
- A die layout with no test data — a map of the reticle or the grid alone — is
|
|
133
|
+
`buildWaferMap({ layout: true, waferConfig, dieConfig })`, not a synthesized results
|
|
134
|
+
array.
|
|
135
|
+
- `valueColorScheme` and `reverseValueScheme` travel as a pair. Every built-in gradient
|
|
136
|
+
but `'traffic'` and `'jet'` reads low = dark, high = light; if you draw your own
|
|
137
|
+
colorbar or swatch, resolve the colour through `resolveValueColorFn(name, reversed)`
|
|
138
|
+
so it cannot disagree with the dies.
|
|
90
139
|
- `result.view` is internal. Use the promoted fields: `result.plotMode`,
|
|
91
140
|
`result.metadata`, `result.isLotStack`, `result.hbinDefs`, `result.sbinDefs`,
|
|
92
141
|
`result.testDefs`.
|
|
@@ -96,8 +145,8 @@ or the other low-level pipeline functions: they are deprecated and removed in 0.
|
|
|
96
145
|
`w.message` to display, branch on `w.code`. Code that calls a string method on an
|
|
97
146
|
entry — `warnings[0].includes('…')` — is the old shape and will throw.
|
|
98
147
|
- **`summary.findings` is the complete list and contains restatements of the same
|
|
99
|
-
fact.** Building a list for a human to read?
|
|
100
|
-
|
|
148
|
+
fact.** Building a list for a human to read? Pass it through `visibleFindings()`,
|
|
149
|
+
which drops what other findings absorb — do not re-implement that filter.
|
|
101
150
|
Skip that and one edge failure is reported up to three times per region — a hard
|
|
102
151
|
bin row, its soft-bin twin, and the yield row that restates the pass bin. Do NOT
|
|
103
152
|
use `relatedIds` for this; it is a different relationship and some ids it names
|
|
@@ -133,6 +182,14 @@ it is handed, because it has no way to know which tests anyone will look at.
|
|
|
133
182
|
- **A Web Worker buys responsiveness, not speed.** `createWafermapWorker` copies data
|
|
134
183
|
across `postMessage`, so total time goes *up*. Use it when a build would otherwise
|
|
135
184
|
visibly freeze the page, not for small datasets.
|
|
185
|
+
- **Capability, pass rates, region yield and the spatial-pattern label come back from
|
|
186
|
+
the analysis** — `stats.capability` (with `computePerTestStats`), `stats.testSpecYield`,
|
|
187
|
+
`stats.testFlagYield`, `stats.functionalYield`, `stats.regionYield` and
|
|
188
|
+
`stats.spatialPattern`, on wafer and lot summaries alike. Do not compute Cp/Cpk or ring yield yourself: the pooled
|
|
189
|
+
within-wafer stddev and per-wafer pass bins are easy to get subtly wrong.
|
|
190
|
+
- **Reports from code: `renderWaferReportHtml(result, summary)` and
|
|
191
|
+
`renderLotReportHtml(results)`** — they take the built maps, so pass bins and ring
|
|
192
|
+
count cannot be wrong. They run in Node.
|
|
136
193
|
- **In a gallery, pass `perWaferSummaries` to `analyzeWaferLot`** so it reuses the
|
|
137
194
|
per-wafer analysis you already ran instead of redoing it.
|
|
138
195
|
|
|
@@ -147,7 +204,7 @@ it is handed, because it has no way to know which tests anyone will look at.
|
|
|
147
204
|
| `WaferMapResult.inference.warnings` | `WaferMapResult.warnings` (structured, with a `code`) |
|
|
148
205
|
| `ViewOptions.testIndex` | `activeTest` |
|
|
149
206
|
| `mountWaferCanvas` | `renderWaferMap` |
|
|
150
|
-
| `HARD_BIN_COLORS` / `SOFT_BIN_COLORS` | `
|
|
207
|
+
| `HARD_BIN_COLORS` / `SOFT_BIN_COLORS` | `BinDef.color`, or `registerBinColorScheme` — there is no exported palette constant |
|
|
151
208
|
| `GalleryItem` | `WaferMapDisplayItem` |
|
|
152
209
|
| `MountOptions` | `RenderOptions` |
|
|
153
210
|
| `WaferCanvasController` | `WaferMapController` |
|
|
@@ -162,6 +219,8 @@ it is handed, because it has no way to know which tests anyone will look at.
|
|
|
162
219
|
| `plotMode: 'specLimit'` | `passFailDisplay: 'spec'` |
|
|
163
220
|
| standalone `getDieAtPoint` | `onHover` / `onClick` on `renderWaferMap` |
|
|
164
221
|
| `RenderOptions.tooltipTestLimit` | (was a no-op; nothing replaces it) |
|
|
222
|
+
| `enableYieldAnalysis` / `enableHardBinAnalysis` / `enableSoftBinAnalysis` / `enableReticlePositionAnalysis` / `enableTestSiteAnalysis` / `enableClusterAnalysis` / `enableAngularAnalysis` / `enablePatternClassification` | nothing — every analysis runs; scope cost with `testNumbers` instead |
|
|
223
|
+
| `WaferMapController.setIdentityVisible` | `showIdentity` in `RenderOptions` |
|
|
165
224
|
|
|
166
225
|
Passing a removed option is a type error, and is ignored at runtime. Do not add
|
|
167
226
|
compatibility shims for them.
|
package/CHANGELOG.md
CHANGED
|
@@ -22,6 +22,253 @@ under `### Breaking`.
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
## [0.30.2] — 2026-09-20
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **A gallery now reports its own progress, so a host's indicator can be honest about a long
|
|
30
|
+
load.** `GalleryOptions` gains two callbacks (API §6.2):
|
|
31
|
+
- `onItemResolved(resolved, total)` — the **advance** signal, fired as each card is built.
|
|
32
|
+
`total` is the count you passed, so it is right from the first call and a progress bar can be
|
|
33
|
+
sized before anything arrives.
|
|
34
|
+
- `onItemsResolved()` — the **settled** signal, fired once the gallery has finished: every
|
|
35
|
+
factory resolved *and* the lot-wide legend, shared colours and Summary panel up to date. On a
|
|
36
|
+
large lot the panel keeps filling in for seconds after the last card, so this is deliberately
|
|
37
|
+
later than "the cards are in" — a host clearing its indicator when the cards land leaves the
|
|
38
|
+
rest of the wait unexplained.
|
|
39
|
+
- Both fire whichever form the items took, so no host branches on the path the gallery chose: a
|
|
40
|
+
fully pre-built mount is one `onItemResolved` call with `resolved === total`, and
|
|
41
|
+
`onItemsResolved` always fires asynchronously, after `renderWaferGallery` has returned. Both
|
|
42
|
+
fire again on a rebuild (`setItems`, or switching into a stacked mode).
|
|
43
|
+
- **`CorrelationMatrix.sample`** — present only when the matrix was computed from a sample of the
|
|
44
|
+
dies, with `of` (the dies that carried test values) and `used` (how many were read).
|
|
45
|
+
The Insights correlation panel labels a sampled matrix from it; a host reading the matrix
|
|
46
|
+
directly must do the same.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **Correlation is computed from at most 25,000 dies.** It is the only analysis here that is
|
|
51
|
+
quadratic in tests and linear in dies — a 400,000-die, 50-test lot is 490 million pair updates,
|
|
52
|
+
which in a browser presents as a panel that never renders. Above that budget the dies are
|
|
53
|
+
sampled by an even stride across the whole population, never a prefix (dies arrive grouped by
|
|
54
|
+
wafer, so the first 25,000 of a lot are its first few wafers and would describe those rather
|
|
55
|
+
than the lot). At 25,000 dies the standard error of `r` is under 0.007, so the coefficients are
|
|
56
|
+
unchanged at the precision they are displayed to. **A sampled matrix always says so** — in the
|
|
57
|
+
Insights panel and in `CorrelationMatrix.sample`.
|
|
58
|
+
- **The lot and wafer Summary panels are built across tasks instead of in one block, and fill in
|
|
59
|
+
section by section.** Pooling every die of every wafer and deriving the bin, region and
|
|
60
|
+
per-test sections from it is the longest single piece of work this library does: 15.3 s on a
|
|
61
|
+
50-wafer lot of 4,000 dies × 100 tests, which is where a browser starts offering to kill the
|
|
62
|
+
page. It is now around 335 steps of a few hundred ms at most. An ordinary lot still renders in
|
|
63
|
+
one synchronous pass and looks exactly as it did. A panel render in flight is abandoned when
|
|
64
|
+
the lot, filter or highlight changes, and on `destroy()`.
|
|
65
|
+
- **The same panel work is also about 2.3× cheaper, and a lot pays for it once.** The per-test
|
|
66
|
+
pass over a lot's pooled dies is memoised on the population it describes, so the lot Summary
|
|
67
|
+
panel, the Insights Overview and the Insights capability chart share one computation instead of
|
|
68
|
+
running three; the sort inside it uses a `Float64Array` (4× on the whole pooled pass); and the
|
|
69
|
+
Test Values rows are built in one pass instead of three.
|
|
70
|
+
- **Switching Insights tabs no longer rebuilds the panel you are returning to.** A built section
|
|
71
|
+
is kept per view, so going back to Distributions is instant rather than re-running capability,
|
|
72
|
+
boxplot, histogram and trend (about 5 s at 400,000 dies). Only a pure tab switch reuses it —
|
|
73
|
+
new data, Group by, scope or an axis preference still rebuilds everything.
|
|
74
|
+
- **A lot-wide option is pushed to the cards only when its value actually changed.** `binColors`,
|
|
75
|
+
`valueRange` and `metadataValueOrder` are re-derived from the whole population as each wafer
|
|
76
|
+
resolves, and each derivation allocates a fresh object, so every card was being rebuilt and
|
|
77
|
+
redrawn for an identical value. On a progressive load that is quadratic: on 50 wafers × 8,000
|
|
78
|
+
dies × 50 tests in Chrome the bin-colour map alone was 1,275 pushes and 24 s of a 64 s load,
|
|
79
|
+
and is now one push.
|
|
80
|
+
- **The gallery's lot Summary panel now settles once, when the last wafer resolves, instead of
|
|
81
|
+
re-rendering after every one.** A progressive load was re-pooling and redrawing the whole
|
|
82
|
+
panel on each newly resolved wafer, which is quadratic in wafer count and was the single
|
|
83
|
+
largest cost on this path: 175 s of a 182 s, 50-card progressive load. The panel still renders
|
|
84
|
+
once at mount and again whenever a user opens it. Combined with the change above, 50 cards of
|
|
85
|
+
8,000 dies now load in **7.7 s of non-blocking work with no task over 440 ms**, down from
|
|
86
|
+
182 s.
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
|
|
90
|
+
- **A large wafer or lot could fail outright with "Maximum call stack size exceeded".**
|
|
91
|
+
`Math.min(...)`/`Math.max(...)` spread a whole die array into a call, which throws once the
|
|
92
|
+
array passes roughly 100,000 entries — reachable on a single 400,000-die wafer in aggregated
|
|
93
|
+
values, inferred wafer geometry, reticle bounds and several chart scales. Every such call now
|
|
94
|
+
uses the `minOf`/`maxOf` helpers, and `scripts/check-spread-limits.mjs` (wired into
|
|
95
|
+
`npm run check`) fails the build if one comes back.
|
|
96
|
+
- **A non-finite test value could make a whole test's statistics `NaN`.** `computePerTestStats`
|
|
97
|
+
sorted and summarised whatever `testValues` held, with no `Number.isFinite` screen, so one
|
|
98
|
+
`NaN` or `Infinity` from a parser silently turned that test's mean, median, sigma and Cpk into
|
|
99
|
+
`NaN` rather than being dropped as no data.
|
|
100
|
+
|
|
101
|
+
## [0.30.1] — 2026-09-16
|
|
102
|
+
|
|
103
|
+
### Security
|
|
104
|
+
|
|
105
|
+
- **Fixes a security issue in how names from a data file are displayed.** Earlier versions are
|
|
106
|
+
affected; update to 0.30.1, particularly if your app opens files from sources you don't
|
|
107
|
+
control. Names now always display as plain text, so a name that contains markup shows it
|
|
108
|
+
literally.
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
|
|
112
|
+
- **Every saved file is named for the data it came from.** Exports were named for their content
|
|
113
|
+
alone — `dies.csv`, `test-values.csv`, `wafermap.png`, `wafer-gallery.png`, a chart's title — so
|
|
114
|
+
the same export from two wafers of a lot saved as `dies.csv` and `dies (1).csv`, with nothing in
|
|
115
|
+
either name to say which wafer it described. Map, gallery and chart PNGs and every CSV export
|
|
116
|
+
are now named `[lot]_[wafer]_[content]`, for example `LOT123_W05_hard-bin.png`,
|
|
117
|
+
`LOT123_W05_die-list.csv` and `LOT123_25-wafers_yield-by-wafer.png` (see API §5.4.5).
|
|
118
|
+
- A part the data doesn't have is left out, never invented. A wafer with no `label` or
|
|
119
|
+
`waferId` gets no wafer part, not a position that could be read as an ID.
|
|
120
|
+
- A gallery writes its lot and wafer counts (`2-lots`, `25-wafers`), and a lot-stacked map
|
|
121
|
+
writes `stacked-N-wafers`. A card or detached window names files for its own wafer.
|
|
122
|
+
- The name is worked out at save time, so it follows `setResult`, `setItems` and plot-mode
|
|
123
|
+
changes.
|
|
124
|
+
- Names are safe on every common filesystem: path and reserved characters are replaced,
|
|
125
|
+
length is limited, and Windows device names are avoided.
|
|
126
|
+
- **`downloadFilename` is unchanged.** When a host sets it, the map or gallery PNG is still
|
|
127
|
+
named `<downloadFilename>.png`. Only its defaults change: without it, the map PNG was
|
|
128
|
+
`wafermap.png` and the gallery PNG `wafer-gallery.png`, and both now get the generated name.
|
|
129
|
+
Gallery cards, which never read it, also get the generated name, as do all CSVs and charts.
|
|
130
|
+
- `onSaveImage` and `onSaveText` receive the generated name as `suggestedName`. A host that
|
|
131
|
+
matched one of the old default names needs to change.
|
|
132
|
+
- The die-list export's content part is `die-list` (was `dies`).
|
|
133
|
+
- **A floating window's minimize button is now Collapse, and is hidden while the window is
|
|
134
|
+
maximized.** In a desktop host such as Tauri, where a detached gallery card falls back to an
|
|
135
|
+
in-page window, a maximized window's header sits directly under the app's own title bar. Its
|
|
136
|
+
`_` minimize looked like the OS button beside it but only shrank the window to a title strip.
|
|
137
|
+
The button is now drawn as chevrons, with the tooltip **Collapse** (**Show contents** while
|
|
138
|
+
collapsed), and a maximized window shows only restore and close, like a modal. This applies to
|
|
139
|
+
the detached-card fallback window and the user guide window. Modals are unchanged, and
|
|
140
|
+
`ICONS.windowMinimize`/`windowRestore` are still exported.
|
|
141
|
+
- **The data layer is about 3 KB larger (~52 KB gzip).** Analysis now returns capability, pass
|
|
142
|
+
rates by recorded verdict and region yield (see Added), so it includes the code that computes
|
|
143
|
+
them.
|
|
144
|
+
|
|
145
|
+
### Deprecated
|
|
146
|
+
|
|
147
|
+
- **`downloadFilename` (on `RenderOptions` and `GalleryOptions`) becomes a prefix in 0.31.0.** It
|
|
148
|
+
will lead the name of every file a map or gallery saves, CSVs included, with the lot, wafer and
|
|
149
|
+
content appended, for example `LOT123_sort_W05_hard-bin.png`. Parts it already names won't be
|
|
150
|
+
repeated, so a host passing a source-file stem that contains the lot won't see the lot twice.
|
|
151
|
+
It is not being removed. Until 0.31.0 it keeps its current meaning, and passing it logs a
|
|
152
|
+
one-time console notice. If you rely on setting a map's whole file name, say so at
|
|
153
|
+
https://github.com/wafertools/wafermap/issues.
|
|
154
|
+
- **Five 0.30.0 deprecations are withdrawn on review**, as each is the only supported path to
|
|
155
|
+
something a host needs: `visibleFindings` (the one rule for collapsing restated findings —
|
|
156
|
+
`summary.findings` is deliberately uncollapsed), `openReportModal` (shows a report when a host
|
|
157
|
+
has installed its own `setReportOpener`), `metadataDisplayValue` (the one rule for a metadata
|
|
158
|
+
value's text in an export), `getReticleCell` (the only source of a die's reticle cell outside
|
|
159
|
+
the tooltip) and `renderFindingsReportHtml` (it takes a summary alone, so it cannot be given
|
|
160
|
+
inconsistent inputs). They log no notice and stay.
|
|
161
|
+
- **Every remaining deprecation notice now names what to use instead**, or says plainly that
|
|
162
|
+
there is no data replacement (the histogram, scatter and correlation builders). The review
|
|
163
|
+
behind each verdict is recorded in the repository's `API_REMOVALS.md`.
|
|
164
|
+
|
|
165
|
+
### Fixed
|
|
166
|
+
|
|
167
|
+
- **A test could not be chosen from the plot-mode menu on a touchscreen.** With more tests than
|
|
168
|
+
fit inline, "Test Value ▶" opens a submenu, and it opened on hover only. On a phone or tablet a
|
|
169
|
+
tap opened the submenu and the same tap closed it again, so the list flashed and no test could
|
|
170
|
+
be picked. From the keyboard, Enter on the row did nothing. The row now also opens the submenu
|
|
171
|
+
when tapped or activated with Enter or Space. Focus moves into the submenu when it is opened from
|
|
172
|
+
the keyboard, the arrow keys move through its tests, and Escape closes it. Tapping the submenu's
|
|
173
|
+
filter box no longer closes the menu underneath. Applies to both `renderWaferMap` and
|
|
174
|
+
`renderWaferGallery`. Guarded by `tests/modeMenuCascade.test.mjs`.
|
|
175
|
+
- **Map lines and markers got thinner as display scaling rose.** Ring, quadrant and reticle
|
|
176
|
+
lines, the wafer outline, the probe path, the +X/+Y indicator, out-of-spec triangles and the
|
|
177
|
+
failing-die hatch were sized in device pixels, so on a 2× display they drew at half their
|
|
178
|
+
intended size and on a 3× display at a third. The ring and quadrant lines' light centre fell
|
|
179
|
+
below one CSS pixel and faded to a faint hairline. They are now sized in CSS pixels, like the
|
|
180
|
+
colorbar's limit markers, so they look the same at every scale and are sharper at higher ones.
|
|
181
|
+
A 1× display draws exactly as before. The die outline stays at its device-pixel width, so
|
|
182
|
+
dense maps on high-DPI screens are not greyed out. Guarded by `tests/strokeScale.test.mjs`.
|
|
183
|
+
- **Outlined buttons lost their border in a dark theme.** Summary report, View die list and the
|
|
184
|
+
Test values / Functional CSV buttons drew a fixed dark edge (`rgba(0,0,0,0.30)`) whatever the
|
|
185
|
+
theme, so on a dark panel they had no visible outline while the ring/quadrant toggles beside
|
|
186
|
+
them did. `--wmap-control-border` now falls back to `--wmap-border` — the edge the
|
|
187
|
+
toggles use — so a theme that sets `--wmap-border` gets the same outline on both. With neither
|
|
188
|
+
set the default is unchanged, and a host that sets `--wmap-control-border` is unaffected.
|
|
189
|
+
- **Bins and test values given as text built a wrong map without a warning.** A CSV parser gives
|
|
190
|
+
every field as a string. String `x`/`y` already made `buildWaferMap` throw, but a bin of `"1"`
|
|
191
|
+
is not pass bin `1`, so those dies counted as fails and yield read 0 %, and a test value of
|
|
192
|
+
`"0.5"` was not plotted or analysed as a number — with nothing to say so. `buildWaferMap` now
|
|
193
|
+
raises an `input-values-not-numbers` warning (severity `error`) counting text bins, text test
|
|
194
|
+
values and non-boolean verdicts, with an example. The values are not converted: convert them
|
|
195
|
+
and rebuild.
|
|
196
|
+
- **Changing the bin palette inside an expanded gallery card did not recolour the map.** A card
|
|
197
|
+
keeps the gallery-wide bin colours so every card agrees, and those took precedence over the
|
|
198
|
+
palette chosen in the card's own menu, so the menu appeared to do nothing — most visibly for soft
|
|
199
|
+
bins. A palette or "Use colours from bin definitions" change made in the card now resolves the
|
|
200
|
+
card's colours from that choice; a change made in the gallery still recolours every card
|
|
201
|
+
together.
|
|
202
|
+
- **A card reattached to the gallery kept the view it had while expanded.** Reattaching stored the
|
|
203
|
+
expanded window's options — plot mode, palettes, overlays — as that card's per-card overrides,
|
|
204
|
+
which win every time the card is built. A card switched to soft bins while expanded stayed a
|
|
205
|
+
soft-bin map under a gallery bar and legend strip describing hard bins, and later gallery palette
|
|
206
|
+
changes left it in colours matching no other card. A reattached card now takes the gallery's
|
|
207
|
+
shared options again, like every other card. This also stops reattaching from discarding
|
|
208
|
+
per-card `viewOptions` the host set on the item.
|
|
209
|
+
- **A lot report built for wafers without a precomputed summary analysed empty wafers.** The
|
|
210
|
+
report ran its lot analysis on `{ label, wafer, dies, passBins }` pieces, which the analysis
|
|
211
|
+
does not recognise as built maps, so it treated each as a fresh input with no results. The
|
|
212
|
+
report then had no findings, `N/A` per-wafer yields, and a title of "Summary — N wafers"
|
|
213
|
+
instead of "Lot Summary — <lot>". It now analyses the built maps (`renderLotReportHtml`, and
|
|
214
|
+
the gallery's and lot panel's report buttons, which use it).
|
|
215
|
+
- **A gallery with a fixed column count left most of the row empty.** Picking 2 columns drew two
|
|
216
|
+
480px cards and left the rest of the row empty, because a fixed column count was still capped
|
|
217
|
+
by die density (since 0.21.1). A fixed column count now divides the full width; Auto keeps the
|
|
218
|
+
cap.
|
|
219
|
+
- **`GalleryOptions.columns` was ignored at mount**, so the grid always drew a single column.
|
|
220
|
+
Invalid counts (`0`, negative, `NaN`) now fall back to automatic layout, and fractions round.
|
|
221
|
+
|
|
222
|
+
### Added
|
|
223
|
+
|
|
224
|
+
Supported replacements for exports deprecated in 0.30.0 that had no other path. Each deprecated
|
|
225
|
+
export's notice names its replacement; the old names are still removed in 0.31.0.
|
|
226
|
+
|
|
227
|
+
- **Capability, pass rates by recorded verdict, and region yield in the analysis output**
|
|
228
|
+
(API §7.4.1), replacing `buildCapabilityData`, `buildTestPassRateData`/`hasJudgeableTests`
|
|
229
|
+
and `buildRegionYieldData` with the ring/quadrant builders:
|
|
230
|
+
- `stats.capability` — Cp/Cpk/Pp/Ppk per parametric test, computed when per-test statistics
|
|
231
|
+
are (`computePerTestStats`). On a lot summary each wafer is a subgroup, so `stdWithin` is the
|
|
232
|
+
pooled within-wafer stddev and Cp and Pp differ. The chart's normalised five-number fields
|
|
233
|
+
are not included; they are not the test's real minimum and quartiles.
|
|
234
|
+
- `stats.testFlagYield` — per-test pass rate by the tester's recorded verdict — and
|
|
235
|
+
`stats.specVerdictDisagreementDies`, the dies where that verdict and the spec-limit judgement
|
|
236
|
+
disagree. They complement the existing `testSpecYield` and `functionalYield`.
|
|
237
|
+
- `stats.regionYield` — `{ ring, quadrant }` yield with each region's die and pass counts, each
|
|
238
|
+
die judged by its own wafer's pass bins.
|
|
239
|
+
- On `LotStatsSummary.stats`: `capability`, `regionYield`, `testSpecYield`, `functionalYield`,
|
|
240
|
+
`testFlagYield` and `specVerdictDisagreementDies`, all pooled exactly from the wafer summaries
|
|
241
|
+
— counts summed, and capability from each wafer's moments — so a lot analysis given
|
|
242
|
+
`perWaferSummaries` stays cheap. A pooled figure is absent unless every wafer reported it. Lot ring yield is also absent when wafers
|
|
243
|
+
were built with different ring counts.
|
|
244
|
+
- `analyzeWaferLot` now builds a raw `WaferMapInput` once and passes the built map to each
|
|
245
|
+
wafer's analysis.
|
|
246
|
+
- **`renderWaferReportHtml(result, summary?)` and `renderLotReportHtml(results)`**, replacing
|
|
247
|
+
`renderSummaryReportHtml` and `renderLotSummaryReportHtml`. They take built maps, so pass bins
|
|
248
|
+
and ring count come from the map rather than defaulting to `[1]` and `4`. They need no DOM.
|
|
249
|
+
The Summary panel's report buttons use them.
|
|
250
|
+
- **`binColorsForMaps(results, options?)` and `getBinColors()` on `WaferMapController` and
|
|
251
|
+
`GalleryController`** — the colours the maps draw, for a host's own table, export or chart —
|
|
252
|
+
replacing `resolveBinColors` and `getBinColorScheme`. `binColorsForMaps` judges each map by its
|
|
253
|
+
own pass bins; the gallery's own colouring now goes through the same function.
|
|
254
|
+
- **`buildWaferMap({ layout: true, waferConfig, dieConfig })`** — a die layout with no test data:
|
|
255
|
+
every site fully on the wafer (the gross die count), die `(0, 0)` at the centre, counted in the
|
|
256
|
+
configured axis directions. It replaces `createWafer` + `generateDies` + `clipDiesToWafer`, and
|
|
257
|
+
renders like any other result.
|
|
258
|
+
- **`stats.spatialPattern` on `analyzeWaferMap`'s result** — the spatial pattern classifier's label,
|
|
259
|
+
confidence and geometry features (failure density overall and at the edge, the failing
|
|
260
|
+
cluster's radial position, eccentricity, linearity, …), replacing a direct `classifyPattern`
|
|
261
|
+
call. It is present for every wafer the classifier can measure, including those labelled
|
|
262
|
+
`'random'` or `'none'`, which raise no finding, so the features can feed a model of your own with
|
|
263
|
+
negative examples as well as patterned ones.
|
|
264
|
+
- **`ICONS.collapse` and `ICONS.uncollapse`**, the floating-window header's collapse and
|
|
265
|
+
show-contents icons.
|
|
266
|
+
- **`WaferMapController.closeSummaryPanel()` and `GalleryController.setColumns()` are restored.**
|
|
267
|
+
Both were removed in 0.30.0 without a deprecation period. A host needs the first to get the
|
|
268
|
+
map's full width back, for example when loading a new file, and the second to apply its own
|
|
269
|
+
column control or a saved preference without rebuilding the gallery. Closing the panel no
|
|
270
|
+
longer resets the Summary button's notable-findings colour, as the 0.29 version did.
|
|
271
|
+
|
|
25
272
|
## [0.30.0] — 2026-09-15
|
|
26
273
|
|
|
27
274
|
### Breaking
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/wafertools/wafermap/actions/workflows/deploy.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/@wafertools/wafermap)
|
|
7
7
|

|
|
8
|
-

|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
|
|
11
11
|
<img src="docs/images/hero-test-values.png" alt="wafermap demo" style="max-width:640px; display:block; margin:8px 0;" />
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
Browser-first wafer map visualization for semiconductor test data.
|
|
14
14
|
|
|
15
15
|
**Zero runtime dependencies.** Pure ES modules with TypeScript types — works in React,
|
|
16
|
-
Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~
|
|
16
|
+
Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~53 kB
|
|
17
17
|
min+gz; the interactive renderer is larger, and its chart suite and in-app guide are
|
|
18
18
|
loaded on demand rather than shipped up front — [measured sizes](docs/performance.md).
|
|
19
19
|
|
|
@@ -72,7 +72,7 @@ export. Add `analyzeWaferMap` for the findings and summary panel, swap in
|
|
|
72
72
|
|
|
73
73
|
The API reference is long because it documents every option, not because you need
|
|
74
74
|
them: [tsmap](https://github.com/wafertools/tsmap), a complete cross-platform desktop
|
|
75
|
-
application built on this library, imports **
|
|
75
|
+
application built on this library, imports **15** of its ~100 exports. Read the
|
|
76
76
|
[Quick Start](https://wafertools.github.io/wafermap/quickstart/) first and treat the
|
|
77
77
|
[API reference](https://wafertools.github.io/wafermap/api/) as something to search,
|
|
78
78
|
not to read.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{SPACE as
|
|
1
|
+
import{SPACE as nt,fontPx as ot,FONT as lt,CLR as it}from"../toolbar.js";import{cardShell as rt,formatValue as st,observeResize as ct,makeTooltip as at,positionChartTooltip as ft,makeBackButton as ht,makeSegmented as dt,growCardToFitContent as ut,resolveChartCanvasColors as mt,PADDING as f,VALUE_WIDTH as z,prepareCanvas as xt}from"./chartShell.js";import{escHtml as _}from"../../core/utils.js";const h=24,m=5,bt=15,N=110,pt=12;export function renderBarPanel(i,U){const{barColor:P,valueLabel:G}=i;let b=i.title,l=i.data;const{card:r,heading:R,controlsRow:I,body:A}=rt(b,U,i.ownerDocument);if(i.selfControl){const e=i.selfControl;I.appendChild(dt(e.options,e.current,n=>q(n),r.ownerDocument))}const{drill:d}=i;let y=!1,B=null;const L=r.ownerDocument.createElement("div");Object.assign(L.style,{color:it.label,fontSize:lt.body,marginBottom:nt.sm}),r.insertBefore(L,A);function D(){const e=[];i.onOpen&&e.push("click to open this wafer"),d&&!y&&e.push(`click a ${d.groupLabelText} to see it by wafer`);const n=e.join(", or ");let o=n?`${n[0].toUpperCase()}${n.slice(1)}.`:"";const t=i.reference?.(l);t&&(o=o?`${o} Dashed line: ${t.label}.`:`Dashed line: ${t.label}.`),L.textContent=o}D();const w=r.ownerDocument.createElement("div"),v=()=>i.reference?bt:0,E=()=>f*2+v()+Math.min(l.length,pt)*(h+m);Object.assign(w.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${E()}px`,scrollbarGutter:"stable"}),A.appendChild(w);const a=r.ownerDocument.createElement("canvas");a.style.display="block",a.style.cursor="default",w.appendChild(a);const k=at(r);let x=-1,M=Math.max(1,...l.map(e=>e.value));const Y=e=>G?G(e):`${st(e.value)} (${e.percent.toFixed(1)}%)`;function F(e){const n=f+v()+e*(h+m),o=f+N,t=a.clientWidth-o-z-f;return{y:n,barX:o,barMaxWidth:Math.max(10,t)}}function p(){w.style.maxHeight=`${E()}px`,ut(r,A,E());const e=w.clientWidth,n=f*2+v()+l.length*(h+m),o=xt(a,r,e,n);if(!o)return;const{ctx:t}=o;t.font=`${ot(-1)}px system-ui, sans-serif`,t.textBaseline="middle";const s=mt(r);l.forEach((c,C)=>{const{y:u,barX:T,barMaxWidth:W}=F(C);C===x&&(t.fillStyle=s.bgHover,t.fillRect(0,u-m/2,e,h+m)),t.fillStyle=s.text,t.textAlign="right";const H=c.label.length>16?`${c.label.slice(0,15)}\u2026`:c.label;t.fillText(H,f+N-8,u+h/2),t.fillStyle=s.track,t.fillRect(T,u,W,h);const S=Math.max(1,c.value/M*W);t.fillStyle=P?P(c,C):s.text,t.fillRect(T,u,S,h),t.fillStyle=s.text,t.textAlign="right",t.fillText(Y(c),T+W+z,u+h/2)});const g=i.reference?.(l);if(g&&l.length){const{barX:c,barMaxWidth:C}=F(0),u=Math.round(c+Math.max(0,Math.min(1,g.value/M))*C)+.5,T=f+v()-3,W=f+v()+l.length*(h+m)-m,H=(Z,tt,et)=>{t.save(),t.strokeStyle=Z,t.lineWidth=tt,t.setLineDash(et),t.beginPath(),t.moveTo(u,T),t.lineTo(u,W),t.stroke(),t.restore()};H(s.bg,3.5,[]),H(s.text,1.5,[4,3]),t.save(),t.font="600 10px system-ui, sans-serif",t.textBaseline="middle",t.textAlign="left";const S=4,O=t.measureText(g.label).width+S*2,X=13,j=f+v()-X-1;let $=u-O/2;$=Math.max(c,Math.min($,c+C-O)),t.fillStyle=s.bg,t.strokeStyle=s.border,t.lineWidth=1,t.beginPath(),t.rect($,j,O,X),t.fill(),t.stroke(),t.fillStyle=s.text,t.fillText(g.label,$+S,j+X/2+.5),t.restore()}}function q(e){if(!i.selfControl)return;i.selfControl.current=e;const n=i.selfControl.onChange(e);l=n.data,M=Math.max(1,...l.map(o=>o.value)),x=-1,n.title&&(b=n.title,R.textContent=b),D(),p()}function J(e){if(!d)return;const n=d.onOpenGroup(e);l=n.data,b=n.title,R.textContent=b,M=Math.max(1,...l.map(o=>o.value)),x=-1,y||(y=!0,B=ht(()=>K(),r.ownerDocument),I.appendChild(B)),D(),p()}function K(){if(!d)return;const e=d.onBack();l=e.data,b=e.title,R.textContent=b,M=Math.max(1,...l.map(n=>n.value)),x=-1,y=!1,B?.remove(),B=null,D(),p()}function V(e){const n=Math.floor((e-f+m/2)/(h+m));return n>=0&&n<l.length?n:-1}a.addEventListener("mousemove",e=>{const n=a.getBoundingClientRect(),o=V(e.clientY-n.top),t=o>=0&&d&&!y&&l[o].itemCount>1,s=o>=0&&(t||!!i.onOpen&&o>=0&&!t);if(o!==x&&(x=o,a.style.cursor=s?"pointer":"default",p()),o>=0){const g=l[o],c=t?`<br><em>click to see this ${_(d.groupLabelText)} by wafer</em>`:i.onOpen?"<br><em>click to open this wafer</em>":"";k.innerHTML=`<strong>${_(g.label)}</strong><br>${_(Y(g))}${c}`,k.style.display="block",ft(k,r,e.clientX,e.clientY)}else k.style.display="none"}),a.addEventListener("mouseleave",()=>{x!==-1&&(x=-1,p()),k.style.display="none"}),a.addEventListener("click",e=>{const n=a.getBoundingClientRect(),o=V(e.clientY-n.top);if(o===-1)return;const t=l[o];if(d&&!y&&t.itemCount>1){J(t);return}i.onOpen?.(t)});const Q=ct(r,()=>p());return p(),{card:r,destroy:()=>Q.disconnect()}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildBinClusterData as
|
|
1
|
+
import{buildBinClusterData as H}from"../../stats/binPareto.js";import{SPACE as T,FONT as w,CLR as E}from"../toolbar.js";import{cardShell as L,makeTooltip as $,makeSegmented as D,renderEmptyState as I}from"./chartShell.js";import{renderGroupedBarPlot as M}from"./groupedBarPlot.js";import{escHtml as g}from"../../core/utils.js";const R=90,_=8;export function renderBinClusterPanel(s){const{groups:f,onSaveImage:h}=s;let n="hbin",c=s.title??"Hard bin pareto";const{card:o,heading:S,body:r,controlsRow:y}=L(c,h,s.ownerDocument);y.appendChild(D([["hbin","Hard bins"],["sbin","Soft bins"]],n,e=>{n=e,c=`${n==="hbin"?"Hard":"Soft"} bin pareto`,S.textContent=c,m()},o.ownerDocument));const u=o.ownerDocument.createElement("div");u.textContent="One cluster per bin \xB7 a sub-bar per group",Object.assign(u.style,{color:E.label,fontSize:w.body,marginBottom:T.sm}),o.insertBefore(u,r);const B=$(o);let l=null;function m(){l?.destroy(),l=null,r.innerHTML="";const e=H(f,n);if(e.bins.length===0||e.groups.length===0){I(r,"No bin data available for the current grouping.");return}const p=e.groups,b=e.bins,C=Math.max(1,...b.flatMap(t=>t.counts));l=M(o,r,B,{rows:b.map(t=>({label:t.label,bars:t.counts.map(a=>({fraction:a/C})),trailing:`${t.total}`})),groups:p,labelWidth:R,maxVisibleRows:_,tooltipHtml:(t,a)=>{const i=b[t],d=i.counts[a],x=i.total>0?d/i.total*100:0;return`<strong>${g(i.label)}</strong> \xB7 ${g(String(p[a]))}<br>${d} dies (${x.toFixed(1)}% of bin)`}})}return m(),{card:o,destroy:()=>l?.destroy()}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildTestBoxplotData as
|
|
1
|
+
import{buildTestBoxplotData as Ze}from"../../stats/boxplot.js";import{SPACE as De,fontPx as N,FONT as et,CLR as tt}from"../toolbar.js";import{fmt as xe}from"../../renderer/fmt.js";import{QUANTITY as Ie}from"./palette.js";import{cardShell as nt,observeResize as ot,makeTooltip as it,positionChartTooltip as lt,makeBackButton as st,makeLinkedTestSelect as at,makeToggle as rt,makeLinkedAxisPrefs as ct,renderEmptyState as He,growCardToFitContent as ft,resolveChartCanvasColors as mt,makeAxisFormat as ut,resolveAxisRange as dt,shouldIncludeLimitsByDefault as xt,drawOffAxisLimits as pt,limitLabelSide as ht,PADDING as L,VALUE_WIDTH as gt,prepareCanvas as bt}from"./chartShell.js";import{escHtml as k,maxOf as Tt,minOf as yt}from"../../core/utils.js";const M=24,C=5,Oe=110,vt=12,Re=20;export function renderBoxplotPanel(h){const{title:Z="Test value distribution",items:We,testDefs:pe,onSaveImage:Ee,groups:u,groupLabelText:ee="group",onOpen:q}=h,Pe=h.openActionLabel??"open that wafer",_e=h.openActionLabel??"open this wafer",{card:a,heading:Ge,body:d,controlsRow:Y}=nt(Z,Ee,h.ownerDocument);a.style.flex="0 0 auto",d.style.flex="0 0 auto",a.style.alignSelf="start";const te=pe.filter(n=>n.testNumber!==void 0);let v=h.selectedTestNumber??te[0]?.testNumber??null,ne=!1,Q=0,x=null,H=null,oe=null,he=null;function Xe(){const n=!u||u.length===0?"":x??"\0overview";if(oe&&he===n)return oe;const m=!u||u.length===0?We:x!==null?u.find(g=>g.key===x)?.items??[]:u.map(g=>({label:g.key,dies:g.items.flatMap(c=>c.dies??[])}));return oe=m,he=n,m}function ge(){x!==null&&!H?(H=st(()=>{be(null),h.onGroupChange?.(null)},a.ownerDocument),Y.appendChild(H)):x===null&&H&&(H.remove(),H=null),Ge.textContent=x!==null?`${Z} \u2014 ${ee}: ${x}`:Z}function be(n){return x===n||n!==null&&!(u??[]).some(m=>m.key===n)?!1:(x=n,ge(),U(),S(),!0)}const Te=at(te,v,n=>{v=n,S(),h.onTestChange?.(n)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Ne=Te.el;Y.appendChild(Ne),Y.appendChild(rt("Log scale",ne,n=>{ne=n,S()},a.ownerDocument));const ie=a.ownerDocument.createElement("span");Object.assign(ie.style,{display:"inline-flex",gap:De.lg,alignItems:"center"}),Y.appendChild(ie);const le=ct(ie,h.axisPrefs,n=>{h.onAxisPrefsChange?.(n),S()},a.ownerDocument),se=a.ownerDocument.createElement("div");Object.assign(se.style,{color:tt.label,fontSize:et.body,marginBottom:De.sm}),a.insertBefore(se,d);function U(){const n=!!u&&u.length>0&&x===null,m=[];n?m.push(`click a ${ee}'s box to see it by wafer`):q&&m.push(`click a box to ${Pe}`);const g=m.length?`${m[0][0].toUpperCase()}${m[0].slice(1)} \xB7 `:"";se.textContent=`${g}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(Q?` \xB7 axis clipped, ${Q} value${Q===1?"":"s"} outside`:"")}U(),ge();const R=it(a);let ae=null;function S(){const{includeLimits:n,clipOutliers:m}=le.get();if(U(),d.innerHTML="",te.length===0||v===null){He(d,"No parametric test data available for box plots.");return}const g=Xe(),c=Ze(g,v),re=pe.find(t=>t.testNumber===v),z=re?.unit,F=re?.limitLow,V=re?.limitHigh;if(c.every(t=>t.count===0)){He(d,"No parametric test data available for box plots.");return}const p=a.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",d.appendChild(p);const ye=L*2+Math.min(c.length,vt)*(M+C)+Re;d.style.maxHeight=`${ye}px`,d.style.overflowY="auto",d.style.scrollbarGutter="stable";let W=-1;const ce=c.filter(t=>t.count>0),ve=yt(ce.map(t=>t.min)),we=Tt(ce.map(t=>t.max)),Le=n??xt(ve,we,F,V);le.sync(Le,F!==void 0||V!==void 0);const E=dt({dataMin:ve,dataMax:we,limitLow:F,limitHigh:V,includeLimits:Le,clipOutliers:m,values:ce.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});Q=E.clippedCount,U();const O=E.lo,fe=E.hi,ke=fe-O||1,j=ne&&O>0,me=j?Math.log10(O):0,Me=(j?Math.log10(fe):0)-me||1,ue=ut(Math.max(Math.abs(O),Math.abs(fe)),z);function Qe(){const t=L+Oe,i=p.clientWidth-t-gt-L;return{plotX:t,plotMaxWidth:Math.max(10,i)}}function A(t,i,s){return j?i+(Math.log10(t)-me)/Me*s:i+(t-O)/ke*s}function Ue(){const t=[];for(let i=0;i<=4;i++)t.push(j?Math.pow(10,me+Me*i/4):O+ke*i/4);return t}function K(){const t=mt(a),i=d.clientWidth,s=L*2+c.length*(M+C)+Re,$=bt(p,a,i,s);if(!$)return;const{ctx:e}=$;e.font=`${N(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:f}=Qe();c.forEach((o,b)=>{const y=L+b*(M+C),r=y+M/2;b===W&&(e.fillStyle=t.bgHover,e.fillRect(0,y-C/2,i,M+C)),e.fillStyle=t.text,e.textAlign="right";const Fe=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(Fe,L+Oe-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const Ve=l+f,_=Je=>Math.min(Ve,Math.max(l,Je)),Ae=A(o.min,l,f),$e=A(o.max,l,f),B=_(Ae),G=_(A(o.q1,l,f)),Be=_(A(o.median,l,f)),de=_(A(o.q3,l,f)),D=_($e),je=Ae<B,Ke=$e>D,I=y+3,X=y+M-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(B,r),e.lineTo(G,r),e.moveTo(de,r),e.lineTo(D,r);const w=3;je?(e.moveTo(B+w,r-w),e.lineTo(B,r),e.lineTo(B+w,r+w)):(e.moveTo(B,I),e.lineTo(B,X)),Ke?(e.moveTo(D-w,r-w),e.lineTo(D,r),e.lineTo(D-w,r+w)):(e.moveTo(D,I),e.lineTo(D,X)),e.stroke(),e.fillStyle=Ie,e.globalAlpha=.35,e.fillRect(G,I,Math.max(1,de-G),X-I),e.globalAlpha=1,e.strokeStyle=Ie,e.strokeRect(G,I,Math.max(1,de-G),X-I),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Be,I),e.lineTo(Be,X),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(xe(o.median,z,"engineering"),l+f+10,r)});const T=L+c.length*(M+C);e.font=`${N(-1)}px system-ui, sans-serif`,pt(e,E.offAxis,{left:l,right:l+f,top:0,bottom:T},"horizontal",t.limitLine,o=>xe(o,z,"engineering"));const ze=new Set(E.offAxis.map(o=>o.value));for(const[o,b]of[[F,"LSL"],[V,"USL"]]){if(o===void 0||ze.has(o))continue;const y=A(o,l,f);e.strokeStyle=t.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(y,0),e.lineTo(y,T),e.stroke(),e.setLineDash([]),e.fillStyle=t.limitLine;const r=ht(y,e.measureText(b).width,l,l+f,b==="LSL");e.textAlign=r<0?"right":"left",e.textBaseline="bottom",e.fillText(b,y+r*3,T-1)}e.font=`${N(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,T),e.lineTo(l+f,T),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";for(const o of Ue()){const b=A(o,l,f);e.beginPath(),e.moveTo(b,T),e.lineTo(b,T+4),e.stroke(),e.fillText(ue.tick(o),b,T+6)}ue.unitLabel&&(e.textAlign="left",e.font=`${N(-1)}px system-ui, sans-serif`,e.fillText(`(${ue.unitLabel})`,l+f+22,T+6),e.font=`${N(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function Ce(t){const i=Math.floor((t-L+C/2)/(M+C));return i>=0&&i<c.length?i:-1}function P(t){return xe(t,z,"engineering")}const J=!!u&&u.length>0&&x===null,Se=t=>!J&&!!q&&g[t]?.waferIndex!==void 0;p.addEventListener("mousemove",t=>{const i=p.getBoundingClientRect(),s=Ce(t.clientY-i.top),$=s>=0&&c[s].count>0&&(J||Se(s));if(s!==W&&(W=s,p.style.cursor=$?"pointer":"default",K()),s>=0&&c[s].count>0){const e=c[s],l=J?`<br><em>click to see this ${k(ee)} by wafer</em>`:Se(s)?`<br><em>click to ${k(_e)}</em>`:"";R.innerHTML=`<strong>${k(e.label)}</strong> (${e.count} dies)<br>max ${k(P(e.max))}<br>q3 ${k(P(e.q3))}<br>median ${k(P(e.median))}<br>q1 ${k(P(e.q1))}<br>min ${k(P(e.min))}${l}`,R.style.display="block",lt(R,a,t.clientX,t.clientY)}else R.style.display="none"}),p.addEventListener("mouseleave",()=>{W!==-1&&(W=-1,K()),R.style.display="none"}),p.addEventListener("click",t=>{const i=p.getBoundingClientRect(),s=Ce(t.clientY-i.top);if(s===-1||c[s].count===0)return;if(J){const e=c[s].label;be(e),h.onGroupChange?.(e);return}const $=g[s]?.waferIndex;q&&$!==void 0&&v!==null&&q($,v)}),ae?.disconnect(),ae=ot(a,()=>K()),K(),ft(a,d,ye)}S();function qe(n){Te.set(n)&&(v=n,S())}function Ye(n){le.set(n)&&S()}return{card:a,setTest:qe,setAxisPrefs:Ye,destroy:()=>ae?.disconnect()}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildCapabilityData as
|
|
1
|
+
import{buildCapabilityData as ue}from"../../stats/capability.js";import{capabilityColor as B}from"./palette.js";import{LEADING as be,SPACE as g,fontPx as G,FONT as Q,CLR as H}from"../toolbar.js";import{cardShell as ye,chartFillHeight as xe,applyCanvasFlow as ge,observeResize as ve,makeTooltip as Se,positionChartTooltip as Me,renderEmptyState as Te,resolveChartCanvasColors as Le,chartSwatchCss as ke,prepareCanvas as Ce,chartDpr as Pe}from"./chartShell.js";import{fmt as $e}from"../../renderer/fmt.js";import{escHtml as ee}from"../../core/utils.js";const Ae=30,We=160,te=90,ne=28,v=42;function I(C){return C===null?"\u2014":C.toFixed(2)}export function renderCapabilityPanel(C){const{title:oe="Process capability",items:le,testDefs:z,onSaveImage:se,onSelectTest:O}=C;let X=C.selectedTestNumber??null;const{card:r,body:S}=ye(oe,se,C.ownerDocument),ie=z.some(n=>n.testNumber!==void 0);S.style.overflowX="auto";function ae(){return le}const P=r.ownerDocument.createElement("div");Object.assign(P.style,{display:"flex",flexDirection:"column",gap:g.xs,marginBottom:g.sm}),r.insertBefore(P,S);const W=Se(r);let R=()=>{};function re(n,i,f){P.innerHTML="";const c=r.ownerDocument.createElement("span");Object.assign(c.style,{display:"inline-flex",alignItems:"center",gap:g.sm,color:H.value,fontSize:Q.body,fontWeight:"500"});const M=f-n,b=i>0?` \xB7 ${i} without spec limits`:"",y=r.ownerDocument.createElement("span");y.textContent=M>0?`${n} of ${f} tests shown${b} \xB7 ${M} excluded (no recorded values)`:`${n} test${n!==1?"s":""} shown${b}`,c.appendChild(y),P.appendChild(c);const $=i>=n,T=r.ownerDocument.createElement("div");T.textContent=$?"No test has both spec limits, so each is normalised to its own observed range (min = 0, max = 1) and sorted most-variable first. Ppk needs limits, so none is shown.":"Normalised to spec limits (LSL = 0, USL = 1), worst Ppk first. Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(T.style,{color:H.label,fontSize:Q.body,lineHeight:be.base,marginTop:g.xs,maxWidth:"78ch"}),P.appendChild(T),P.appendChild(ce(r.ownerDocument,i>0,$))}function ce(n,i,f=!1){const c=n.createElement("div");Object.assign(c.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${g.xs} ${g.xl}`,marginTop:g.xl,marginBottom:g.xs,fontSize:Q.body,color:H.label});const M=f?[]:[{color:B(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:B(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:B(.5),label:"Poor \xB7 Ppk < 1.0"}];i&&M.push({color:H.label,label:"No spec limits \xB7 no Ppk",dashed:!0});for(const b of M){const y=n.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:g.sm});const $=n.createElement("span");$.style.cssText=ke(b.color,b.dashed?"outline":"solid");const T=n.createElement("span");T.textContent=b.label,y.append($,T),c.appendChild(y)}return c}function me(n){if(S.innerHTML="",n.length===0)return Te(S,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const i=r.ownerDocument.createElement("canvas");i.style.display="block",i.style.cursor="default",S.appendChild(i);const f=n.length,c=Math.min(0,...n.map(t=>t.min)),M=Math.max(1,...n.map(t=>t.max)),b=(M-c)*.05||.1,y=c-b,T=M+b-y||1;function V(t){return Math.max(Ae,Math.min(We,Math.floor(t/f)))}function L(t,s,o){return s+(1-(t-y)/T)*o}function he(t,s,o,l,k){const x=n.filter(a=>a.hasSpec).length,A=x===n.length,d=x===0,_=d?"max":"USL",e=d?"min":"LSL",h=A?"normalised to spec":d?"normalised to range":"normalised (per test)";t.font=`${G(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle";for(const[a,D]of[[1,_],[0,e]]){const p=L(a,o,l);p<o-1||p>o+l+1||(t.strokeStyle=s.limitLine,t.lineWidth=1,t.beginPath(),t.moveTo(v-5,p),t.lineTo(v,p),t.stroke(),t.fillStyle=s.textMuted,t.fillText(D,v-8,p))}t.save(),t.translate(9,o+l/2),t.rotate(-Math.PI/2),t.textAlign="center",t.fillStyle=s.textMuted,t.fillText(h,0,0),t.restore()}let w=-1;function j(){ge(i);const t=S.clientWidth,s=V(t-v),o=s*f,l=ne,k=v+o,x=xe(r,S,i,ne+200+te),A=Math.max(l+60,x-te),d=A-l,_=Ce(i,r,k,x);if(!_)return;const{ctx:e}=_,h=Le(r);n.forEach((a,D)=>{const p=v+D*s,m=p+s/2,u=Math.max(4,s*.55);if(D===w&&(e.fillStyle=h.bgHover,e.fillRect(p,l,s,A-l)),n[D].testNumber===X&&(e.fillStyle=h.accent,e.fillRect(p,A-2,s,2)),a.hasSpec){const K=L(0,l,d),Z=L(1,l,d);e.strokeStyle=h.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(p,K),e.lineTo(p+s,K),e.moveTo(p,Z),e.lineTo(p+s,Z),e.stroke(),e.setLineDash([])}const F=a.hasSpec?B(a.ppk):h.textMuted,U=L(a.min,l,d),E=L(a.q1,l,d),J=L(a.median,l,d),N=L(a.q3,l,d),q=L(a.max,l,d);e.strokeStyle=h.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(m,U),e.lineTo(m,E),e.moveTo(m,N),e.lineTo(m,q),e.moveTo(m-u/4,U),e.lineTo(m+u/4,U),e.moveTo(m-u/4,q),e.lineTo(m+u/4,q),e.stroke(),e.fillStyle=F,e.globalAlpha=.5,e.fillRect(m-u/2,Math.min(E,N),u,Math.max(1,Math.abs(N-E))),e.globalAlpha=1,e.strokeStyle=F,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(m-u/2,Math.min(E,N),u,Math.max(1,Math.abs(N-E))),e.setLineDash([]),e.strokeStyle=h.text,e.lineWidth=2,e.beginPath(),e.moveTo(m-u/2,J),e.lineTo(m+u/2,J),e.stroke(),e.lineWidth=1,e.font=`${G(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?F:h.textMuted,e.fillText(a.hasSpec?I(a.ppk):"\u2014",m,l-8);const fe=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(m,A+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${G(-1)}px system-ui, sans-serif`,e.fillStyle=h.textMuted,e.fillText(fe,0,0),e.restore()}),e.strokeStyle=h.border,e.lineWidth=1,e.strokeRect(v,l,o,d),he(e,h,l,d,o)}function Y(t){const s=i.getBoundingClientRect(),o=S.clientWidth,l=V(o-v),k=(t.clientX-s.left)*(i.width/Pe(i)/s.width)-v,x=Math.floor(k/l);return x>=0&&x<f?x:-1}return i.addEventListener("mousemove",t=>{const s=Y(t);if(s!==w&&(w=s,j()),s===-1){W.style.display="none";return}const o=n[s],l=k=>ee($e(k,o.unit));W.innerHTML=`<strong>${ee(o.label)}</strong> (n=${o.n})<br>`+(o.hasSpec?`LSL ${l(o.lsl)} \xB7 USL ${l(o.usl)}<br>mean ${l(o.mean)}<br>Cp ${I(o.cp)} \xB7 Cpk ${I(o.cpk)}<br>Pp ${I(o.pp)} \xB7 Ppk ${I(o.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${l(o.mean)} \xB7 stddev ${l(o.stdOverall)}`)+(O?"<br><em>click to view in boxplot</em>":""),W.style.display="block",Me(W,r,t.clientX,t.clientY),i.style.cursor=O?"pointer":"default"}),i.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,j()),W.style.display="none"}),i.addEventListener("click",t=>{if(!O)return;const s=Y(t);s!==-1&&O(n[s].testNumber)}),j}function de(){const n=ue(ae(),z),i=z.filter(c=>c.testNumber!==void 0).length,f=n.filter(c=>!c.hasSpec).length;re(n.length,f,i),R=me(n),R()}const pe=ve(r,()=>R());return de(),{card:r,hasData:ie,setTest:n=>{X!==n&&(X=n,R())},destroy:()=>pe.disconnect()}}
|