@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.
Files changed (86) hide show
  1. package/AGENTS.md +68 -9
  2. package/CHANGELOG.md +247 -0
  3. package/README.md +3 -3
  4. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  9. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  10. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +1 -0
  11. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  13. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  15. package/dist/packages/canvas-adapter/chunked.d.ts +45 -0
  16. package/dist/packages/canvas-adapter/chunked.js +1 -0
  17. package/dist/packages/canvas-adapter/deprecated.d.ts +0 -3
  18. package/dist/packages/canvas-adapter/deprecated.js +1 -1
  19. package/dist/packages/canvas-adapter/dieList.js +1 -1
  20. package/dist/packages/canvas-adapter/exportName.d.ts +68 -0
  21. package/dist/packages/canvas-adapter/exportName.js +1 -0
  22. package/dist/packages/canvas-adapter/icons.js +2 -2
  23. package/dist/packages/canvas-adapter/index.d.ts +2 -1
  24. package/dist/packages/canvas-adapter/index.js +1 -1
  25. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  26. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +80 -3
  27. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  28. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +24 -2
  29. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  30. package/dist/packages/canvas-adapter/summaryPanel.d.ts +76 -15
  31. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  32. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  33. package/dist/packages/canvas-adapter/toolbar.d.ts +6 -0
  34. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  35. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  36. package/dist/packages/canvas-adapter/userGuideHtml.js +15 -8
  37. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  38. package/dist/packages/canvas-adapter/version.js +1 -1
  39. package/dist/packages/canvas-adapter/warnings.js +1 -1
  40. package/dist/packages/core/aggregates.js +1 -1
  41. package/dist/packages/core/deprecated.d.ts +8 -12
  42. package/dist/packages/core/deprecated.js +1 -1
  43. package/dist/packages/core/index.d.ts +1 -1
  44. package/dist/packages/core/index.js +1 -1
  45. package/dist/packages/core/utils.d.ts +67 -0
  46. package/dist/packages/core/utils.js +1 -1
  47. package/dist/packages/core/waferLabel.d.ts +12 -2
  48. package/dist/packages/core/waferLabel.js +1 -1
  49. package/dist/packages/renderer/binColors.d.ts +52 -0
  50. package/dist/packages/renderer/binColors.js +1 -1
  51. package/dist/packages/renderer/buildView.js +1 -1
  52. package/dist/packages/renderer/buildWaferMap.d.ts +30 -5
  53. package/dist/packages/renderer/buildWaferMap.js +1 -1
  54. package/dist/packages/renderer/deprecate.d.ts +7 -0
  55. package/dist/packages/renderer/deprecate.js +1 -1
  56. package/dist/packages/renderer/deprecated.d.ts +2 -2
  57. package/dist/packages/renderer/deprecated.js +1 -1
  58. package/dist/packages/renderer/index.d.ts +2 -1
  59. package/dist/packages/renderer/index.js +1 -1
  60. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  61. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  62. package/dist/packages/stats/capability.d.ts +72 -1
  63. package/dist/packages/stats/capability.js +1 -1
  64. package/dist/packages/stats/correlation.d.ts +30 -0
  65. package/dist/packages/stats/correlation.js +1 -1
  66. package/dist/packages/stats/deprecated.d.ts +30 -35
  67. package/dist/packages/stats/deprecated.js +1 -1
  68. package/dist/packages/stats/findingsNarrative.js +1 -1
  69. package/dist/packages/stats/index.d.ts +4 -2
  70. package/dist/packages/stats/index.js +1 -1
  71. package/dist/packages/stats/math.d.ts +27 -2
  72. package/dist/packages/stats/math.js +1 -1
  73. package/dist/packages/stats/normalizeInput.d.ts +10 -0
  74. package/dist/packages/stats/normalizeInput.js +1 -0
  75. package/dist/packages/stats/regions.d.ts +2 -0
  76. package/dist/packages/stats/regions.js +1 -1
  77. package/dist/packages/stats/renderFindingsReport.js +1 -1
  78. package/dist/packages/stats/renderSummaryReport.d.ts +39 -1
  79. package/dist/packages/stats/renderSummaryReport.js +27 -27
  80. package/dist/packages/stats/reportHtml.d.ts +0 -1
  81. package/dist/packages/stats/reportHtml.js +10 -10
  82. package/dist/packages/stats/summaryFigures.d.ts +47 -0
  83. package/dist/packages/stats/summaryFigures.js +1 -0
  84. package/dist/packages/stats/types.d.ts +118 -0
  85. package/llms.txt +12 -7
  86. 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. Do not use `toCanvas()`, `buildView()`
29
- or the other low-level pipeline functions: they are deprecated and removed in 0.31.0.
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
- - **`passBins` decides both the yield number and the wording of its label.** Set it
41
- from the actual test program. Do not assume `[1]`.
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? Exclude what other findings absorb:
100
- `const absorbed = new Set(summary.findings.flatMap(f => f.absorbedIds ?? []))`.
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` | `BIN_PALETTE` |
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
  [![CI and deploy](https://github.com/wafertools/wafermap/actions/workflows/deploy.yml/badge.svg)](https://github.com/wafertools/wafermap/actions/workflows/deploy.yml)
6
6
  [![npm](https://img.shields.io/npm/v/@wafertools/wafermap.svg)](https://www.npmjs.com/package/@wafertools/wafermap)
7
7
  ![runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)
8
- ![bundle](https://img.shields.io/badge/data%20layer%20min%2Bgz-~49%20kB-blue)
8
+ ![bundle](https://img.shields.io/badge/data%20layer%20min%2Bgz-~53%20kB-blue)
9
9
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./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 ~49 kB
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 **14** of its ~100 exports. Read the
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 et,fontPx as nt,FONT as ot,CLR as lt}from"../toolbar.js";import{cardShell as it,formatValue as rt,observeResize as st,makeTooltip as ct,positionChartTooltip as at,makeBackButton as ft,makeSegmented as ht,growCardToFitContent as dt,resolveChartCanvasColors as ut,PADDING as f,VALUE_WIDTH as j,prepareCanvas as xt}from"./chartShell.js";const h=24,x=5,mt=15,z=110,bt=12;export function renderBarPanel(i,N){const{barColor:_,valueLabel:P}=i;let b=i.title,l=i.data;const{card:r,heading:R,controlsRow:G,body:A}=it(b,N,i.ownerDocument);if(i.selfControl){const e=i.selfControl;G.appendChild(ht(e.options,e.current,n=>U(n),r.ownerDocument))}const{drill:d}=i;let y=!1,B=null;const L=r.ownerDocument.createElement("div");Object.assign(L.style,{color:lt.label,fontSize:ot.body,marginBottom:et.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?mt:0,E=()=>f*2+v()+Math.min(l.length,bt)*(h+x);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=ct(r);let m=-1,M=Math.max(1,...l.map(e=>e.value));const I=e=>P?P(e):`${rt(e.value)} (${e.percent.toFixed(1)}%)`;function Y(e){const n=f+v()+e*(h+x),o=f+z,t=a.clientWidth-o-j-f;return{y:n,barX:o,barMaxWidth:Math.max(10,t)}}function p(){w.style.maxHeight=`${E()}px`,dt(r,A,E());const e=w.clientWidth,n=f*2+v()+l.length*(h+x),o=xt(a,r,e,n);if(!o)return;const{ctx:t}=o;t.font=`${nt(-1)}px system-ui, sans-serif`,t.textBaseline="middle";const s=ut(r);l.forEach((c,C)=>{const{y:u,barX:T,barMaxWidth:W}=Y(C);C===m&&(t.fillStyle=s.bgHover,t.fillRect(0,u-x/2,e,h+x)),t.fillStyle=s.text,t.textAlign="right";const S=c.label.length>16?`${c.label.slice(0,15)}\u2026`:c.label;t.fillText(S,f+z-8,u+h/2),t.fillStyle=s.track,t.fillRect(T,u,W,h);const $=Math.max(1,c.value/M*W);t.fillStyle=_?_(c,C):s.text,t.fillRect(T,u,$,h),t.fillStyle=s.text,t.textAlign="right",t.fillText(I(c),T+W+j,u+h/2)});const g=i.reference?.(l);if(g&&l.length){const{barX:c,barMaxWidth:C}=Y(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+x)-x,S=(Q,Z,tt)=>{t.save(),t.strokeStyle=Q,t.lineWidth=Z,t.setLineDash(tt),t.beginPath(),t.moveTo(u,T),t.lineTo(u,W),t.stroke(),t.restore()};S(s.bg,3.5,[]),S(s.text,1.5,[4,3]),t.save(),t.font="600 10px system-ui, sans-serif",t.textBaseline="middle",t.textAlign="left";const $=4,O=t.measureText(g.label).width+$*2,X=13,V=f+v()-X-1;let H=u-O/2;H=Math.max(c,Math.min(H,c+C-O)),t.fillStyle=s.bg,t.strokeStyle=s.border,t.lineWidth=1,t.beginPath(),t.rect(H,V,O,X),t.fill(),t.stroke(),t.fillStyle=s.text,t.fillText(g.label,H+$,V+X/2+.5),t.restore()}}function U(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)),m=-1,n.title&&(b=n.title,R.textContent=b),D(),p()}function q(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)),m=-1,y||(y=!0,B=ft(()=>J(),r.ownerDocument),G.appendChild(B)),D(),p()}function J(){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)),m=-1,y=!1,B?.remove(),B=null,D(),p()}function F(e){const n=Math.floor((e-f+x/2)/(h+x));return n>=0&&n<l.length?n:-1}a.addEventListener("mousemove",e=>{const n=a.getBoundingClientRect(),o=F(e.clientY-n.top),t=o>=0&&d&&!y&&l[o].itemCount>1,s=o>=0&&(t||!!i.onOpen&&o>=0&&!t);if(o!==m&&(m=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>${I(g)}${c}`,k.style.display="block",at(k,r,e.clientX,e.clientY)}else k.style.display="none"}),a.addEventListener("mouseleave",()=>{m!==-1&&(m=-1,p()),k.style.display="none"}),a.addEventListener("click",e=>{const n=a.getBoundingClientRect(),o=F(e.clientY-n.top);if(o===-1)return;const t=l[o];if(d&&!y&&t.itemCount>1){q(t);return}i.onOpen?.(t)});const K=st(r,()=>p());return p(),{card:r,destroy:()=>K.disconnect()}}
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 x}from"../../stats/binPareto.js";import{SPACE as T,FONT as w,CLR as E}from"../toolbar.js";import{cardShell as H,makeTooltip as L,makeSegmented as $,renderEmptyState as D}from"./chartShell.js";import{renderGroupedBarPlot as I}from"./groupedBarPlot.js";const M=90,R=8;export function renderBinClusterPanel(s){const{groups:g,onSaveImage:f}=s;let n="hbin",c=s.title??"Hard bin pareto";const{card:o,heading:h,body:r,controlsRow:S}=H(c,f,s.ownerDocument);S.appendChild($([["hbin","Hard bins"],["sbin","Soft bins"]],n,e=>{n=e,c=`${n==="hbin"?"Hard":"Soft"} bin pareto`,h.textContent=c,d()},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 y=L(o);let a=null;function d(){a?.destroy(),a=null,r.innerHTML="";const e=x(g,n);if(e.bins.length===0||e.groups.length===0){D(r,"No bin data available for the current grouping.");return}const p=e.groups,b=e.bins,B=Math.max(1,...b.flatMap(t=>t.counts));a=I(o,r,y,{rows:b.map(t=>({label:t.label,bars:t.counts.map(l=>({fraction:l/B})),trailing:`${t.total}`})),groups:p,labelWidth:M,maxVisibleRows:R,tooltipHtml:(t,l)=>{const i=b[t],m=i.counts[l],C=i.total>0?m/i.total*100:0;return`<strong>${i.label}</strong> \xB7 ${p[l]}<br>${m} dies (${C.toFixed(1)}% of bin)`}})}return d(),{card:o,destroy:()=>a?.destroy()}}
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 Je}from"../../stats/boxplot.js";import{SPACE as Be,fontPx as X,FONT as Ze,CLR as et}from"../toolbar.js";import{fmt as de}from"../../renderer/fmt.js";import{QUANTITY as De}from"./palette.js";import{cardShell as tt,observeResize as nt,makeTooltip as ot,positionChartTooltip as it,makeBackButton as lt,makeLinkedTestSelect as st,makeToggle as at,makeLinkedAxisPrefs as rt,renderEmptyState as Ie,growCardToFitContent as ct,resolveChartCanvasColors as ft,makeAxisFormat as ut,resolveAxisRange as mt,shouldIncludeLimitsByDefault as dt,drawOffAxisLimits as xt,limitLabelSide as pt,PADDING as L,VALUE_WIDTH as ht,prepareCanvas as gt}from"./chartShell.js";const M=24,k=5,Re=110,bt=12,He=20;export function renderBoxplotPanel(h){const{title:J="Test value distribution",items:Oe,testDefs:xe,onSaveImage:We,groups:m,groupLabelText:Z="group",onOpen:N}=h,Ee=h.openActionLabel??"open that wafer",Pe=h.openActionLabel??"open this wafer",{card:a,heading:_e,body:d,controlsRow:q}=tt(J,We,h.ownerDocument);a.style.flex="0 0 auto",d.style.flex="0 0 auto",a.style.alignSelf="start";const ee=xe.filter(n=>n.testNumber!==void 0);let v=h.selectedTestNumber??ee[0]?.testNumber??null,te=!1,Y=0,x=null,I=null,ne=null,pe=null;function Ge(){const n=!m||m.length===0?"":x??"\0overview";if(ne&&pe===n)return ne;const u=!m||m.length===0?Oe:x!==null?m.find(g=>g.key===x)?.items??[]:m.map(g=>({label:g.key,dies:g.items.flatMap(c=>c.dies??[])}));return ne=u,pe=n,u}function he(){x!==null&&!I?(I=lt(()=>{ge(null),h.onGroupChange?.(null)},a.ownerDocument),q.appendChild(I)):x===null&&I&&(I.remove(),I=null),_e.textContent=x!==null?`${J} \u2014 ${Z}: ${x}`:J}function ge(n){return x===n||n!==null&&!(m??[]).some(u=>u.key===n)?!1:(x=n,he(),Q(),C(),!0)}const be=st(ee,v,n=>{v=n,C(),h.onTestChange?.(n)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Xe=be.el;q.appendChild(Xe),q.appendChild(at("Log scale",te,n=>{te=n,C()},a.ownerDocument));const oe=a.ownerDocument.createElement("span");Object.assign(oe.style,{display:"inline-flex",gap:Be.lg,alignItems:"center"}),q.appendChild(oe);const ie=rt(oe,h.axisPrefs,n=>{h.onAxisPrefsChange?.(n),C()},a.ownerDocument),le=a.ownerDocument.createElement("div");Object.assign(le.style,{color:et.label,fontSize:Ze.body,marginBottom:Be.sm}),a.insertBefore(le,d);function Q(){const n=!!m&&m.length>0&&x===null,u=[];n?u.push(`click a ${Z}'s box to see it by wafer`):N&&u.push(`click a box to ${Ee}`);const g=u.length?`${u[0][0].toUpperCase()}${u[0].slice(1)} \xB7 `:"";le.textContent=`${g}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(Y?` \xB7 axis clipped, ${Y} value${Y===1?"":"s"} outside`:"")}Q(),he();const H=ot(a);let se=null;function C(){const{includeLimits:n,clipOutliers:u}=ie.get();if(Q(),d.innerHTML="",ee.length===0||v===null){Ie(d,"No parametric test data available for box plots.");return}const g=Ge(),c=Je(g,v),ae=xe.find(t=>t.testNumber===v),U=ae?.unit,z=ae?.limitLow,F=ae?.limitHigh;if(c.every(t=>t.count===0)){Ie(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 Te=L*2+Math.min(c.length,bt)*(M+k)+He;d.style.maxHeight=`${Te}px`,d.style.overflowY="auto",d.style.scrollbarGutter="stable";let O=-1;const re=c.filter(t=>t.count>0),ye=Math.min(...re.map(t=>t.min)),ve=Math.max(...re.map(t=>t.max)),we=n??dt(ye,ve,z,F);ie.sync(we,z!==void 0||F!==void 0);const W=mt({dataMin:ye,dataMax:ve,limitLow:z,limitHigh:F,includeLimits:we,clipOutliers:u,values:re.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});Y=W.clippedCount,Q();const R=W.lo,ce=W.hi,Le=ce-R||1,V=te&&R>0,fe=V?Math.log10(R):0,Me=(V?Math.log10(ce):0)-fe||1,ue=ut(Math.max(Math.abs(R),Math.abs(ce)),U);function Ye(){const t=L+Re,i=p.clientWidth-t-ht-L;return{plotX:t,plotMaxWidth:Math.max(10,i)}}function S(t,i,s){return V?i+(Math.log10(t)-fe)/Me*s:i+(t-R)/Le*s}function Qe(){const t=[];for(let i=0;i<=4;i++)t.push(V?Math.pow(10,fe+Me*i/4):R+Le*i/4);return t}function j(){const t=ft(a),i=d.clientWidth,s=L*2+c.length*(M+k)+He,A=gt(p,a,i,s);if(!A)return;const{ctx:e}=A;e.font=`${X(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:f}=Ye();c.forEach((o,b)=>{const y=L+b*(M+k),r=y+M/2;b===O&&(e.fillStyle=t.bgHover,e.fillRect(0,y-k/2,i,M+k)),e.fillStyle=t.text,e.textAlign="right";const ze=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(ze,L+Re-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const Fe=l+f,P=Ke=>Math.min(Fe,Math.max(l,Ke)),Se=S(o.min,l,f),Ae=S(o.max,l,f),$=P(Se),_=P(S(o.q1,l,f)),$e=P(S(o.median,l,f)),me=P(S(o.q3,l,f)),B=P(Ae),Ve=Se<$,je=Ae>B,D=y+3,G=y+M-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo($,r),e.lineTo(_,r),e.moveTo(me,r),e.lineTo(B,r);const w=3;Ve?(e.moveTo($+w,r-w),e.lineTo($,r),e.lineTo($+w,r+w)):(e.moveTo($,D),e.lineTo($,G)),je?(e.moveTo(B-w,r-w),e.lineTo(B,r),e.lineTo(B-w,r+w)):(e.moveTo(B,D),e.lineTo(B,G)),e.stroke(),e.fillStyle=De,e.globalAlpha=.35,e.fillRect(_,D,Math.max(1,me-_),G-D),e.globalAlpha=1,e.strokeStyle=De,e.strokeRect(_,D,Math.max(1,me-_),G-D),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo($e,D),e.lineTo($e,G),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(de(o.median,U,"engineering"),l+f+10,r)});const T=L+c.length*(M+k);e.font=`${X(-1)}px system-ui, sans-serif`,xt(e,W.offAxis,{left:l,right:l+f,top:0,bottom:T},"horizontal",t.limitLine,o=>de(o,U,"engineering"));const Ue=new Set(W.offAxis.map(o=>o.value));for(const[o,b]of[[z,"LSL"],[F,"USL"]]){if(o===void 0||Ue.has(o))continue;const y=S(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=pt(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=`${X(-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 Qe()){const b=S(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=`${X(-1)}px system-ui, sans-serif`,e.fillText(`(${ue.unitLabel})`,l+f+22,T+6),e.font=`${X(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function ke(t){const i=Math.floor((t-L+k/2)/(M+k));return i>=0&&i<c.length?i:-1}function E(t){return de(t,U,"engineering")}const K=!!m&&m.length>0&&x===null,Ce=t=>!K&&!!N&&g[t]?.waferIndex!==void 0;p.addEventListener("mousemove",t=>{const i=p.getBoundingClientRect(),s=ke(t.clientY-i.top),A=s>=0&&c[s].count>0&&(K||Ce(s));if(s!==O&&(O=s,p.style.cursor=A?"pointer":"default",j()),s>=0&&c[s].count>0){const e=c[s],l=K?`<br><em>click to see this ${Z} by wafer</em>`:Ce(s)?`<br><em>click to ${Pe}</em>`:"";H.innerHTML=`<strong>${e.label}</strong> (${e.count} dies)<br>max ${E(e.max)}<br>q3 ${E(e.q3)}<br>median ${E(e.median)}<br>q1 ${E(e.q1)}<br>min ${E(e.min)}${l}`,H.style.display="block",it(H,a,t.clientX,t.clientY)}else H.style.display="none"}),p.addEventListener("mouseleave",()=>{O!==-1&&(O=-1,j()),H.style.display="none"}),p.addEventListener("click",t=>{const i=p.getBoundingClientRect(),s=ke(t.clientY-i.top);if(s===-1||c[s].count===0)return;if(K){const e=c[s].label;ge(e),h.onGroupChange?.(e);return}const A=g[s]?.waferIndex;N&&A!==void 0&&v!==null&&N(A,v)}),se?.disconnect(),se=nt(a,()=>j()),j(),ct(a,d,Te)}C();function Ne(n){be.set(n)&&(v=n,C())}function qe(n){ie.set(n)&&C()}return{card:a,setTest:Ne,setAxisPrefs:qe,destroy:()=>se?.disconnect()}}
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 fe}from"../../stats/capability.js";import{capabilityColor as B}from"./palette.js";import{LEADING as ue,SPACE as g,fontPx as G,FONT as Q,CLR as H}from"../toolbar.js";import{cardShell as be,chartFillHeight as ye,applyCanvasFlow as xe,observeResize as ge,makeTooltip as ve,positionChartTooltip as Se,renderEmptyState as Me,resolveChartCanvasColors as Te,chartSwatchCss as Le,prepareCanvas as ke,chartDpr as Ce}from"./chartShell.js";import{fmt as Pe}from"../../renderer/fmt.js";const $e=30,Ae=160,ee=90,te=28,v=42;function I(C){return C===null?"\u2014":C.toFixed(2)}export function renderCapabilityPanel(C){const{title:ne="Process capability",items:oe,testDefs:z,onSaveImage:le,onSelectTest:O}=C;let X=C.selectedTestNumber??null;const{card:r,body:S}=be(ne,le,C.ownerDocument),se=z.some(n=>n.testNumber!==void 0);S.style.overflowX="auto";function ie(){return oe}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=ve(r);let R=()=>{};function ae(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:ue.base,marginTop:g.xs,maxWidth:"78ch"}),P.appendChild(T),P.appendChild(re(r.ownerDocument,i>0,$))}function re(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=Le(b.color,b.dashed?"outline":"solid");const T=n.createElement("span");T.textContent=b.label,y.append($,T),c.appendChild(y)}return c}function ce(n){if(S.innerHTML="",n.length===0)return Me(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($e,Math.min(Ae,Math.floor(t/f)))}function L(t,s,o){return s+(1-(t-y)/T)*o}function pe(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(){xe(i);const t=S.clientWidth,s=V(t-v),o=s*f,l=te,k=v+o,x=ye(r,S,i,te+200+ee),A=Math.max(l+60,x-ee),d=A-l,_=ke(i,r,k,x);if(!_)return;const{ctx:e}=_,h=Te(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 he=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(he,0,0),e.restore()}),e.strokeStyle=h.border,e.lineWidth=1,e.strokeRect(v,l,o,d),pe(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/Ce(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=>Pe(k,o.unit);W.innerHTML=`<strong>${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",Se(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 me(){const n=fe(ie(),z),i=z.filter(c=>c.testNumber!==void 0).length,f=n.filter(c=>!c.hasSpec).length;ae(n.length,f,i),R=ce(n),R()}const de=ge(r,()=>R());return me(),{card:r,hasData:se,setTest:n=>{X!==n&&(X=n,R())},destroy:()=>de.disconnect()}}
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()}}