@wafertools/wafermap 0.28.0 → 0.30.0

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 (66) hide show
  1. package/AGENTS.md +13 -14
  2. package/CHANGELOG.md +291 -0
  3. package/README.md +5 -5
  4. package/dist/packages/canvas-adapter/deprecated.d.ts +10 -0
  5. package/dist/packages/canvas-adapter/deprecated.js +1 -0
  6. package/dist/packages/canvas-adapter/dieList.d.ts +1 -3
  7. package/dist/packages/canvas-adapter/dieList.js +7 -7
  8. package/dist/packages/canvas-adapter/index.d.ts +1 -3
  9. package/dist/packages/canvas-adapter/index.js +1 -1
  10. package/dist/packages/canvas-adapter/insightsTab.d.ts +0 -1
  11. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  12. package/dist/packages/canvas-adapter/maplessSummary.d.ts +3 -0
  13. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  14. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +1 -40
  15. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  16. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +81 -104
  17. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  18. package/dist/packages/canvas-adapter/summaryPanel.d.ts +11 -8
  19. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  20. package/dist/packages/canvas-adapter/toCanvas.d.ts +20 -12
  21. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  22. package/dist/packages/canvas-adapter/toolbar.d.ts +2 -0
  23. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  24. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  25. package/dist/packages/canvas-adapter/userGuideHtml.js +14 -10
  26. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  27. package/dist/packages/canvas-adapter/version.js +1 -1
  28. package/dist/packages/canvas-adapter/warnings.js +1 -1
  29. package/dist/packages/core/deprecated.d.ts +64 -0
  30. package/dist/packages/core/deprecated.js +1 -0
  31. package/dist/packages/core/index.d.ts +1 -3
  32. package/dist/packages/core/index.js +1 -1
  33. package/dist/packages/core/passBins.d.ts +33 -0
  34. package/dist/packages/core/passBins.js +1 -0
  35. package/dist/packages/core/waferLabel.d.ts +25 -0
  36. package/dist/packages/core/waferLabel.js +1 -0
  37. package/dist/packages/renderer/binColors.d.ts +67 -13
  38. package/dist/packages/renderer/binColors.js +1 -1
  39. package/dist/packages/renderer/buildView.d.ts +23 -9
  40. package/dist/packages/renderer/buildView.js +1 -1
  41. package/dist/packages/renderer/buildWaferMap.d.ts +47 -13
  42. package/dist/packages/renderer/buildWaferMap.js +1 -1
  43. package/dist/packages/renderer/colorMap.d.ts +4 -3
  44. package/dist/packages/renderer/colorSchemes.d.ts +21 -6
  45. package/dist/packages/renderer/colorSchemes.js +1 -1
  46. package/dist/packages/renderer/deprecate.d.ts +24 -0
  47. package/dist/packages/renderer/deprecate.js +1 -0
  48. package/dist/packages/renderer/deprecated.d.ts +45 -0
  49. package/dist/packages/renderer/deprecated.js +1 -0
  50. package/dist/packages/renderer/index.d.ts +2 -3
  51. package/dist/packages/renderer/index.js +1 -1
  52. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  53. package/dist/packages/stats/binPareto.d.ts +6 -1
  54. package/dist/packages/stats/clusterDetection.d.ts +0 -2
  55. package/dist/packages/stats/deprecated.d.ts +87 -0
  56. package/dist/packages/stats/deprecated.js +1 -0
  57. package/dist/packages/stats/index.d.ts +3 -14
  58. package/dist/packages/stats/index.js +1 -1
  59. package/dist/packages/stats/regions.d.ts +7 -2
  60. package/dist/packages/stats/regions.js +1 -1
  61. package/dist/packages/stats/renderSummaryReport.d.ts +3 -0
  62. package/dist/packages/stats/renderSummaryReport.js +34 -34
  63. package/dist/packages/stats/types.d.ts +0 -43
  64. package/dist/packages/stats/yield.d.ts +12 -6
  65. package/dist/packages/stats/yield.js +1 -1
  66. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -20,14 +20,13 @@ loudly over guessing.
20
20
  ### Entry points
21
21
 
22
22
  - `@wafertools/wafermap` — `buildWaferMap()`, geometry, `registerBinColorScheme()` / `registerValueColorScheme()`. Pure, no DOM, server-safe.
23
- - `@wafertools/wafermap/render` — `renderWaferMap()`, `renderWaferGallery()`, `toCanvas()`. Needs the DOM.
23
+ - `@wafertools/wafermap/render` — `renderWaferMap()`, `renderWaferGallery()`. Needs the DOM.
24
24
  - `@wafertools/wafermap/stats` — `analyzeWaferMap()`, `analyzeWaferLot()`. Pure analysis.
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. Reach for `toCanvas()`/`buildView()`
29
- only when you need the low-level pipeline — they give up the toolbar and every UI
30
- correctness guarantee that comes with it.
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.
31
30
 
32
31
  ### Traps that produce silently wrong maps
33
32
 
@@ -65,7 +64,6 @@ correctness guarantee that comes with it.
65
64
  the wrong place. The renderers surface these themselves in a toolbar indicator, so
66
65
  do NOT hand-roll a second display — pass
67
66
  `warnings: { display: false, onWarning }` if the app has its own notification UI.
68
- (`result.inference.warnings` is a deprecated string mirror; do not use it.)
69
67
  - **Give the container a resolved height.** `renderWaferMap` fills its container.
70
68
  A bare block-flow `<div>` is fine — it grows to the canvas. The real failure is a
71
69
  flex/grid child whose ancestors never resolve a height: it stays 0-tall and the
@@ -82,11 +80,10 @@ correctness guarantee that comes with it.
82
80
  - `retestPolicy: 'best'`/`'worst'` is pass/fail-aware via `passBins`; bin number only
83
81
  breaks ties within a category.
84
82
  - Hard bins (`hbin`) and soft bins (`sbin`) are independent number spaces. Never merge them.
85
- - **Never pick a bin's colour from its number.** Bin colours are assigned by
86
- `resolveBinColors` — pass bins (per `passBins`) take green pass colours, fail bins
87
- take the rest, most populous first — and a rendered map exposes the result as
88
- `View.binColors`. A legend or chart you build yourself must read those, or it
89
- will name colours the map is not drawing. Bin maps and value maps have separate
83
+ - **Do not colour bins yourself.** The maps give each bin one colour from its number
84
+ and its pass/fail verdict (pass bins, per `passBins`, take green pass colours), so a
85
+ bin is the same colour in every lot. To choose colours, set `BinDef.color` or register
86
+ a palette with `registerBinColorScheme`. Bin maps and value maps have separate
90
87
  schemes: `binColorScheme` and `valueColorScheme`.
91
88
  - Build once, render many: `buildWaferMap()` handles data + geometry; re-render UI
92
89
  changes through the controller's `setOptions()`, not by rebuilding.
@@ -146,6 +143,8 @@ it is handed, because it has no way to know which tests anyone will look at.
146
143
  | `DieResult.values` / `Die.values` | `testValues` (keyed by test number) |
147
144
  | `TestDef.index` | `TestDef.testNumber` (required) |
148
145
  | `ViewOptions.colorBySpec` | `passFailDisplay: 'spec'` |
146
+ | `View.colorBySpec` | `view.passFailDisplay` |
147
+ | `WaferMapResult.inference.warnings` | `WaferMapResult.warnings` (structured, with a `code`) |
149
148
  | `ViewOptions.testIndex` | `activeTest` |
150
149
  | `mountWaferCanvas` | `renderWaferMap` |
151
150
  | `HARD_BIN_COLORS` / `SOFT_BIN_COLORS` | `BIN_PALETTE` |
@@ -153,15 +152,15 @@ it is handed, because it has no way to know which tests anyone will look at.
153
152
  | `MountOptions` | `RenderOptions` |
154
153
  | `WaferCanvasController` | `WaferMapController` |
155
154
  | `CanvasHitTarget` | `HitTarget` |
156
- | `buildScene` / `BuildSceneOptions` / `SceneOptions` | `buildView` / `ViewOptions` |
155
+ | `buildScene` / `BuildSceneOptions` / `SceneOptions` | `renderWaferMap` |
157
156
  | `WaferFlat`, field `flat` | `WaferNotch`, field `notch` |
158
- | `isInsideWaferWithFlat` | `isInsideWafer` |
157
+ | `isInsideWaferWithFlat` | nothing — `buildWaferMap` resolves the geometry |
159
158
  | `DieSample` / `WaferMapPoint` | `DieResult` |
160
159
  | `colorScheme` / `WaferViewOptions.colorScheme` | `binColorScheme` (bin maps) and `valueColorScheme` (value and stacked maps) |
161
160
  | `registerColorScheme` / `getColorScheme` / `listColorSchemes` | `registerBinColorScheme` / `registerValueColorScheme` and their `get` / `list` pairs |
162
- | `hardBinColor` / `softBinColor` / `hardBinGreyscale` | `resolveBinColors`, or `View.binColors` from a rendered map |
161
+ | `hardBinColor` / `softBinColor` / `hardBinGreyscale` | `BinDef.color`, or `registerBinColorScheme` |
163
162
  | `plotMode: 'specLimit'` | `passFailDisplay: 'spec'` |
164
- | standalone `getDieAtPoint` | `hitTarget.getDieAtPoint` from `toCanvas()` |
163
+ | standalone `getDieAtPoint` | `onHover` / `onClick` on `renderWaferMap` |
165
164
  | `RenderOptions.tooltipTestLimit` | (was a no-op; nothing replaces it) |
166
165
 
167
166
  Passing a removed option is a type error, and is ignored at runtime. Do not add
package/CHANGELOG.md CHANGED
@@ -22,6 +22,297 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
25
+ ## [0.30.0] — 2026-09-15
26
+
27
+ ### Breaking
28
+
29
+ - **Bin colour is keyed by bin number again, not by die count.** Since 0.28.0,
30
+ `resolveBinColors` ranked bins by die count and handed out palette colours in that order, so
31
+ a colour meant "the biggest fail bin in this view" rather than any particular bin: two lots
32
+ of one program drew hard bin 7 red in one and brown in the other, and filtering a gallery
33
+ could recolour bins. Industry wafer-map tools key colour on the bin number so engineers can
34
+ learn a program's colours and compare screenshots; wmap now does the same. A pass bin takes
35
+ `pass[(bin − 1) mod n]` and a fail bin `fail[(bin − 2) mod n]`, so bin 1 is the first green and
36
+ bin 2 is red in the default palette. Still pass/fail-aware (a failing bin 1 is never green),
37
+ still honours `BinDef.color`. The slot comes from the number alone, so changing `passBins`
38
+ recolours only bins whose verdict changed.
39
+ - **Soft bins read the palette shifted by half its length**, so hard bin *n* and soft bin *n*
40
+ are different colours by default.
41
+ - **`shared` now reports fixed pairs:** bins whose numbers are a palette-length apart (fail
42
+ bins 2 and 21 in `'default'`, 2 and 16 in `'accessible'`) when both are present, rather than
43
+ "more bins than colours".
44
+ - Same function, same types, different colours for the same input, which is why this is
45
+ breaking. Any host screenshot or test that pinned a rank-derived colour will change.
46
+ - **The `passBins` options on `analyzeWaferMap`, `analyzeWaferLot`, `renderWaferMap` and
47
+ `renderWaferGallery` are removed.** Pass bins are set once, on `buildWaferMap`'s input, and
48
+ carried on the result as `WaferMapResult.passBins` (new), which all four read — per wafer in a
49
+ gallery or lot. A second place to set them is how every surface came to judge pass/fail by `[1]`
50
+ (see Fixed), and an analysis override produced a summary whose findings and yield figure used
51
+ different pass bins. A map not built by `buildWaferMap` states them in that same `passBins`
52
+ field. Passing a removed option is a type error; at runtime `analyzeWaferMap` and
53
+ `analyzeWaferLot` ignore it and say so with an `analysis-option-corrected` warning, as they do
54
+ for a stale `ringCount`. To analyse or render with different pass bins, rebuild.
55
+ - **Options and methods removed as unused, duplicated or dead.** Found by measuring what tsmap,
56
+ the examples and the library itself actually use; each was either never set by anything, a
57
+ second way to set something that already had one, or accepted and ignored.
58
+ - `RenderOptions`: `minZoom`/`maxZoom` (fixed at 0.4× and 20× of the fitted view),
59
+ `renderTooltip`, `maxSize`, `toolbarControls` (its `'view-only'` value was never used),
60
+ `showPlotModeSelector`, and the top-level `legendPosition` and `fallbackFormat` — set both
61
+ through `viewOptions`.
62
+ - `GalleryOptions`: `cardPadding`, `maxSize` (the density-derived card size cap always
63
+ applies), `showPlotModeSelector`, `legendPosition` and `fallbackFormat` (through `viewOptions`).
64
+ - `WaferMapController`: `setDies` (it replaced dies but not the wafer geometry, so switching
65
+ wafers with it kept the previous wafer's outline — use `setResult`), `setFallbackFormat` (use
66
+ `setOptions({ fallbackFormat })`), `setHelpButtonVisible`, `setTooltipParent`.
67
+ `GalleryController`: `setFallbackFormat`, `setColumns`.
68
+ - `ViewOptions.dieGap` (fixed at 1 mm), `ToCanvasOptions.topClearance` (always passed 0),
69
+ `DieListDisplayOptions.csvFilename`.
70
+ - `AnalyzeWaferMapOptions.includePartial` / `includeEdgeExcluded`: analysing dies that yield
71
+ excludes could only make the findings describe a different population from the yield figure
72
+ beside them. `isYieldEligibleDie`'s own options are unchanged.
73
+ - `fallbackFormat` is now a view preference (`WaferPreferences.fallbackFormat`), reported
74
+ through `onViewOptionsChange` with the others.
75
+ - **Gallery-to-card plumbing is off the public API.** `WaferDisplayState` loses `binColors`,
76
+ `metadataValueOrder` and `lotSize`; `RenderOptions` loses `chromeInset` and `onExpand`;
77
+ `WaferMapController` loses `setSummaryVisible` and `setViewControlsVisible`. The gallery set
78
+ all of them on its own cards, and on a host's map they read as settings a host must supply to
79
+ get a valid map — a lone map resolves its bin colours, metadata order and lot size itself.
80
+ `renderWaferGallery`'s `getOptions()` and `onViewOptionsChange` no longer carry the three
81
+ fields either (`binColors` held `Map`s, which a host persisting its options could not
82
+ serialise). `ViewOptions.binColors`, `.metadataValueOrder` and `.lotSize` for `buildView` are
83
+ unchanged.
84
+ - **Ring count is set once, on `buildWaferMap`.** `WaferMapInput.ringCount` (default 4) is
85
+ carried as `WaferMapResult.ringCount`, and the ring overlay, ring findings, Summary panel,
86
+ report and Insights all read it. `AnalyzeWaferMapOptions.ringCount` and the view preference
87
+ `ringCount` are removed: they were two places that had to match, and when they did not, the
88
+ ring boundaries on the map described different rings from the ring findings beside them.
89
+ Validation moved with it — a bad value is corrected and reported in `result.warnings` as
90
+ `analysis-option-corrected`, still with no upper bound. A gallery whose wafers were built with
91
+ different ring counts raises the new `ring-count-mixed` warning; each card and each wafer's
92
+ findings use their own. `ViewOptions.ringCount` (for `buildView`) and `classifyDie`'s option
93
+ are unchanged.
94
+ - **`BinColors` has a new required field, `pass: { hard: Set<number>; soft: Set<number> }`** —
95
+ the bins that pass, per type, as `resolveBinColors` judged them. A host that builds a
96
+ `BinColors` by hand must add it; an object without it passed as `ViewOptions.binColors` is
97
+ now ignored and resolved afresh rather than trusted. `binPassSets(dies, passBins)` gives the
98
+ same sets without colours.
99
+ - **`WaferMapResult.inference.warnings` is removed.** It has been a deprecated string mirror since
100
+ 0.13.5, when `WaferMapResult.warnings` replaced it with the same messages plus a stable `code` and
101
+ a severity. Read `result.warnings`. Internally each geometry advisory now records its code where it
102
+ is detected, rather than having it recovered by matching phrases in the message.
103
+ - **`View.colorBySpec` is removed.** Its input option went in 0.21.0; the output stayed, only ever
104
+ equal to `passFailDisplay === 'spec'`. Read `view.passFailDisplay`.
105
+ - **`ToCanvasOptions.activeBin`, `.hoverBin` and `.minRightReserve` are removed.** They carry the
106
+ interactive map's own legend highlight, pointer and mode-switch layout state, which a direct
107
+ `toCanvas` caller has none of — the same reason `RenderOptions` already dropped them.
108
+ - **`AnalyzeWaferMapOptions` loses its per-analysis switches:** `enableYieldAnalysis`, `enableHardBinAnalysis`, `enableSoftBinAnalysis`, `enableReticlePositionAnalysis`, `enableTestSiteAnalysis`, `enableClusterAnalysis`, `enableAngularAnalysis` and `enablePatternClassification`.
109
+ Each was cheap and on by default; every analysis now runs, and a caller wanting fewer findings
110
+ filters them (`filterFindings`). Passing one from untyped JavaScript is reported as
111
+ `analysis-option-corrected`. The two that cost real time stay: `enableTestValueAnalysis`, and
112
+ `computePerTestStats`, which triples analysis time on a 7,843-die, 50-test wafer (94 ms → 298 ms)
113
+ and so cannot simply always run.
114
+ - **`WaferMapController.setExpandVisible`, `setIdentityVisible`, `closeSummaryPanel` and
115
+ `getActiveLegend` are removed.** The first two duplicated the `showExpandButton` and `showIdentity`
116
+ options, and nothing used the other two. The gallery keeps expand-button control internally.
117
+
118
+ ### Fixed
119
+
120
+ - **A wafer with no die positions covered the Insights view.** In `renderWaferMap`, a
121
+ coordinate-less wafer's "No die position data" summary stayed on top of the chart suite once
122
+ Insights was opened, hiding most of it. A mixed wafer's "+N dies without position data" footer
123
+ did the same. Their z-index beat the Insights layer's. The map view is now hidden outright while
124
+ Insights is open, so nothing inside it can show through, and its controls leave the tab order.
125
+
126
+ - **Input names removed in earlier releases vanished without a trace.** A plain-JavaScript caller
127
+ still passing `data` instead of `results` got an empty map, and `values` instead of `testValues`
128
+ a map with no test data — no error, no warning, nothing that looked wrong. `buildWaferMap` now
129
+ reports every removed name it finds (`data`, `die`, `stack`, `values`, `TestDef.index`,
130
+ `dieConfig.origin`, `waferConfig.flat`, `reticleConfig.anchor`, `lotStack.aggr`) as a new
131
+ `input-field-removed` warning naming each replacement, in `result.warnings` and on the console.
132
+ It still does not honour them: rename and rebuild.
133
+ - **Pass bins given to `buildWaferMap` were used for `result.yield` and nothing else.** The result
134
+ had nowhere to keep them, so every later surface fell back to `[1]` unless the caller repeated
135
+ them: `analyzeWaferMap`'s findings and yield statistics, bin colours and legend order, the
136
+ failing-die hatch, the Summary panel and report, region yield, the Insights yield charts, the
137
+ gallery strip's Yield, and the map's own internal view. A program whose bins 1 and 2 both pass
138
+ therefore showed bin 2 as a failure everywhere except the one yield figure — and the docs
139
+ already claimed `analyzeWaferMap` inferred them from the result. tsmap hit this on every file
140
+ whose pass bins are not just bin 1: it gives each wafer's pass bins to `buildWaferMap` and to
141
+ nothing else. `[1]` is now a default only at `buildWaferMap`'s input; `WaferMapResult.passBins`
142
+ carries the value everywhere after.
143
+ - **A gallery judges each wafer by its own pass bins**, so a lot mixing test programs is
144
+ correct per wafer in cards, strip yield, lot panel, report, region yield and Insights. Hard
145
+ bins that pass on one wafer and fail on another get one colour and one legend row, so the
146
+ gallery raises `pass-bins-mixed` naming them. Yield labels say "per wafer: bin 1 · bins 1, 2"
147
+ when wafers disagree, rather than naming one wafer's set.
148
+ - The report's lot region-yield table and the gallery strip's yield were separate copies of
149
+ the per-die pass rule; both now use the shared ones (`buildRegionYieldData`, `diePassStatus`).
150
+ - **A map's bin legend drew its title over the first row when there were many bins.** Rows
151
+ were fitted to the full canvas height and the "Soft Bin"/"Hard Bin" title placed above them
152
+ afterwards, clamped below the toolbar, so a legend tall enough to fill the height had its
153
+ title printed across row one. The title row (and a floating legend's padding and heading) is
154
+ now reserved before rows are fitted; overflow moves into "+ N more" one row sooner.
155
+ - **Bin legends list pass bins first, then failing bins by die count**, the order the Summary
156
+ panel, report and Insights pareto already used (`sortBinsForDisplay`). The per-map canvas
157
+ legend and the gallery's legend strip both sorted by bin number, so one lot was listed two
158
+ ways on one screen, and a program with dozens of bins buried its biggest failures.
159
+ - **Soft bins were ordered, and the gallery's soft-bin yield was totalled, as if hard pass bin
160
+ numbers applied to them.** The Summary panel, both report bin tables and the gallery strip
161
+ judged a soft bin "pass" by looking it up in `passBins`, which holds hard-bin numbers: soft bin
162
+ 1 sorted as a pass and soft bin 100 as a fail whatever their dies did, and the gallery strip's
163
+ Yield read about 0% in soft-bin mode for any program whose passing soft bins are numbered
164
+ differently. They now use the soft bin's own verdict (`BinColors.pass.soft`).
165
+ - **A gallery card had no title unless the host passed `label`, and every other surface named
166
+ that wafer differently.** Without `label` the card header was blank, so an engineer could not
167
+ tell which wafer a card showed. The findings list, the lot report, the yield list and the die
168
+ list's Wafer column called it `W3`, a position that reads like a wafer ID and need not match
169
+ the real one; a detached window called it "Wafer map"; and the Insights tab used the wafer ID.
170
+ The wafer ID was on every item. One internal rule, `waferDisplayLabel` (`core/waferLabel.ts`),
171
+ now names a wafer everywhere: the host's `label`, else `wafer.metadata.waferId`, else
172
+ "Wafer 3 (no ID)". Hosts that pass `label` see no change.
173
+ - **The gallery's Legend style menu offered per-card legend positions as if they applied to
174
+ a legend nobody could see.** Per-card legends are off by default in `renderWaferGallery`
175
+ (the lot legend strip stands in for them), yet the menu listed the six positions first,
176
+ live, with **Default (right)** ticked, and the **Legend on each map** toggle last. It read
177
+ as though the positions moved the lot strip. The toggle now comes first, and the positions
178
+ follow under **Position on each map**, greyed and unticked, with the reason as a tooltip,
179
+ until per-card legends are on. The single-map menu, which has no toggle, is unchanged.
180
+ The rule is in `makeLegendStyleBtn` (`toolbar.ts`).
181
+
182
+ ### Changed
183
+
184
+ - **Segments of the gallery legend's population bar are now separated by a 1px gap.**
185
+ Segments are ordered by die count, so any two palette colours can sit side by side, and at
186
+ 8px tall the darker ones (black, indigo, dark teal, brown) and the pass greens ran together,
187
+ so the bar read as fewer, wider bins than it held. The gap is transparent, so
188
+ it shows the strip's own background in either theme, and it adds to each segment's 2px
189
+ minimum width rather than eating into it.
190
+
191
+ ### Deprecated
192
+
193
+ - **`valueToViridis`, `valueToGreyscale` and `getValueColorScheme`**, to be removed in
194
+ 0.31.0. Each still works, logs one console notice on first use, and is struck
195
+ through in editors. Use `resolveValueColorFn(name, reversed)` instead:
196
+ `resolveValueColorFn('default')` and `resolveValueColorFn('greyscale')` return exactly the
197
+ colours the first two did. None of the three applies `reverseValueScheme`, so a legend, chart
198
+ or export built on them could show a reading in a different colour from the map.
199
+ `listValueColorSchemes()` gives scheme names and labels.
200
+ - **The chart-data builders**, to be removed in 0.31.0: `buildYieldData`,
201
+ `buildYieldDataCombined`, `buildBinParetoData`, `buildBinClusterData`, `buildCapabilityData`,
202
+ `buildTestBoxplotData`, `buildTestTrendData`, `trendCentre`, `buildTestPassRateData`,
203
+ `hasJudgeableTests`, `buildTestHistogramData`, `buildTestHistogramSeries`,
204
+ `buildCorrelationMatrix`, `filterCorrelationMatrix`, `buildScatterData` and
205
+ `buildScatterDataGrouped`. They were made public so a host could draw the Insights charts
206
+ itself, before the Insights tab (`insights: { enabled: true }`) drew them; neither tsmap nor
207
+ any example calls them. Each still works and logs one console notice on first use. There is
208
+ no replacement: if you depend on one, say so at https://github.com/wafertools/wafermap/issues. Their types stay until the
209
+ functions go. `buildFacetTable`, `facetValueOf` and `mergeTestDefs` are not
210
+ deprecated.
211
+ - **The low-level drawing pipeline**, to be removed in 0.31.0: `buildView`, `toCanvas`, `createWafer`,
212
+ `generateDies`, `clipDiesToWafer`, `applyOrientation`, `transformDies`, `applyProbeSequence`,
213
+ `generateReticleGrid`, `mapDataToDies`, `isInsideWafer`, `getReticleCell`, `resolveGridPitch`,
214
+ `classifyDie`, `getRingLabel`, `aggregateValues`, `aggregateBinCounts`, `getUniqueBins`,
215
+ `buildHoverText`, `buildMapTitle` and the eight `affine*` helpers. It let a host draw a map without
216
+ `renderWaferMap`; nothing known does — tsmap draws every map through the renderers — and only the
217
+ pipeline example used it, which is removed. The types only the pipeline needs (`ViewOptions`,
218
+ `ToCanvasOptions` and the rest) go with it.
219
+ - **Helpers exported by accident**, to be removed in 0.31.0: the region builders (`buildRingRegions`,
220
+ `buildQuadrantRegions`, `buildSectorRegions`, `buildReticlePositionRegions`, `buildTestSiteRegions`,
221
+ `buildRegionYieldData`, `areQuadrantsAdjacent`, `parseRegionKey`, `sectorCompassNames`),
222
+ `classifyPattern`, `visibleFindings`, `computeFunctionalYield`, `resolveMetadataColumns`,
223
+ `discoverDieMetadataKeys`, the report builders (`renderSummaryReportHtml`,
224
+ `renderLotSummaryReportHtml`, `renderFindingsReportHtml`, `openHtmlReport`, `openReportModal`),
225
+ `resolveBinColors`, `getBinColorScheme`, `contrastTextColor`, `getDieTestValue`, `dieHasTestData`,
226
+ `isParametricTest`, `isPositionedDie`, `metadataDisplayValue`, `metadataCategoricalValue`,
227
+ `buildDieListSection`, `DEFAULT_FACET_CURATION` and `STANDARD_WAFER_DIAMETERS_MM`. The library
228
+ applies each itself. Staying, because each carries a rule or a hook a host needs: `getDieKey`,
229
+ `getTestPassStatus`, `diePassStatus`, `isYieldEligibleDie`, `hasPosition`, `filterFindings`,
230
+ `resolveValueColorFn`, `FACET_NONE_VALUE`, `setReportOpener` and `setDetachWindowOpener`. The two
231
+ constants cannot log a notice; the functions do.
232
+ - **The 0.31.0 removal is enforced**, not just announced: a test fails once the changelog or
233
+ `package.json` reaches 0.31.0 while any of these exports is still there.
234
+
235
+ ### Docs
236
+
237
+ - **New example, "Bin colours across a full program"** (`docs/examples/bin-colours.html`).
238
+ A four-wafer gallery over a new generated dataset, `showcase-bin-rich`: 15 hard bins with two
239
+ pass grades and gaps in the numbering, and 32 soft bins numbered under their hard bin. Each
240
+ failure mode has its own spatial pattern. Every other bundled dataset has 2–6 bins, which is
241
+ too few to show how bin colour behaves across a real program. The soft bins deliberately
242
+ outnumber the palette, so the example also shows the `bin-colors-shared` warning. The data
243
+ comes from `scripts/gen-showcase-csvs.mjs` like the other showcase files.
244
+
245
+ ### Internal
246
+
247
+ - **CI runs once per push to `main`, not twice.** `ci.yml` had its own `push` trigger while
248
+ `deploy.yml`, which also runs on every push, calls it as its gating job, so the same suite
249
+ ran twice in parallel on each commit. `ci.yml` now triggers only on pull requests (and as
250
+ a reusable workflow). `deploy.yml` is therefore the only thing testing pushes to `main`,
251
+ and says so. The README badge now points at `deploy.yml`.
252
+ - **The perf suite's ratio tests use the fastest of N runs instead of the median**
253
+ (`fastest()` in `tests/perf.test.mjs`). The two paths in a ratio are timed one after the
254
+ other, so background load inflated them unequally. The value-vs-hardBin check failed an
255
+ `npm publish` at 2.86× on a loaded machine where both paths had slowed about 7×. Absolute
256
+ budget tests keep the median, where a minimum would weaken the guard. The value-mode limit
257
+ stays at 2.5×: measured, the steadier statistic does not buy back enough headroom under
258
+ CPU saturation to tighten it.
259
+
260
+ ## [0.29.0] — 2026-09-12
261
+
262
+ ### Breaking
263
+
264
+ - **The default value gradient is now Viridis, and every perceptual gradient reads low = dark,
265
+ high = light.** The blue–cyan–yellow–red "thermal" ramp is removed. Like every rainbow ramp its
266
+ lightness is not monotonic — cyan and yellow both sit near peak while blue and red are much
267
+ darker — so two different readings land at the same apparent intensity, and the fast hue turns
268
+ at cyan and yellow draw contour lines that are not in the data. On a smooth parametric map that
269
+ shows up as a bright ring around the wafer centre that no process step put there. Users reported
270
+ exactly this: being unable to judge the difference between two colours that look equally
271
+ "intense". `'jet'` keeps the rainbow family available for anyone who wants it, already labelled
272
+ as such.
273
+ - **`viridis`, `cividis`, `plasma`, `inferno` and `greyscale` were registered reversed, and are
274
+ no longer.** Each was `forValue: t => ramp(1 - t)`, putting the *bright* end of the ramp at the
275
+ *low* end of the data, while `default`, `traffic` and `jet` ran the other way. Switching gradient
276
+ therefore inverted the map. On a stacked map the effect was backwards from what the mode is for:
277
+ the healthy bulk of the wafer (fail count 0) rendered as a glowing yellow field and the edge
278
+ ring, scratch or cluster you were looking for became dark specks on it. All five now match their
279
+ matplotlib/seaborn definitions. `tests/colorSchemes.test.mjs` measures L\* across every
280
+ perceptual built-in and fails if a reversal reappears.
281
+
282
+ This also corrects the record on the change that made thermal the default in the first place
283
+ (0.14.x, "the reversed-Viridis ramp rendered high values dark purple and low values yellow — not
284
+ intuitive"). The diagnosis was right and the remedy was aimed at the wrong thing: the defect was
285
+ the `1 - t`, not Viridis, and the `1 - t` survived the change that was meant to fix it.
286
+ - **The standalone `'viridis'` gradient name is gone** — Viridis *is* `'default'`, labelled
287
+ **Default (Viridis)**. Two menu rows drawing identical maps is the same defect that removed the
288
+ old standalone "Thermal" row. `getValueColorScheme('viridis')` falls back to `'default'`, so a
289
+ persisted `'viridis'` preference still renders Viridis; it no longer appears in
290
+ `listValueColorSchemes()`.
291
+ - **`View.reverseValueScheme` is a new required field on `View`.** Code that builds a `View`
292
+ literal must supply it; code that reads one is unaffected.
293
+
294
+ ### Added
295
+
296
+ - **`reverseValueScheme` (`ViewOptions`, `WaferPreferences`) — flips whichever gradient is
297
+ selected**, offered in the Colour scheme menu as **Reverse gradient**. One flag rather than a
298
+ reversed twin of every ramp: it doubles no menu, and a gradient a host registered itself gets
299
+ the behaviour for free. It is also what makes the direction rule above exception-free —
300
+ greyscale ships low = dark like everything else, and the print habit of "more ink means more" is
301
+ one tick box rather than a documented special case.
302
+ - **`resolveValueColorFn(name, reversed)` (`renderer`) — the single read-path for a value
303
+ gradient.** Die fills, the colorbar and the mapless summary's histogram bars each looked the
304
+ gradient up independently; a flag applied at some of them and not others would show one reading
305
+ in two different colours on one screen, with the colorbar — the thing the map is read against —
306
+ the likeliest to be missed. All three now resolve through this. Any host colouring by value
307
+ should too, passing `View.valueColorScheme` and `View.reverseValueScheme` together.
308
+ - **`'mako'` value gradient** (seaborn), sampled at nine even stops from
309
+ `sns.color_palette('mako', as_cmap=True)`. The closest thing to Viridis in lightness span, with
310
+ more separation at the top end. Its sibling `crest` was measured alongside it and deliberately
311
+ not added: at L\* ~31 → ~78 against Viridis's ~10 → ~93 it has too little span for a dense die
312
+ grid — on a stacked map a scratch all but disappeared into the surrounding green. Seaborn pitches
313
+ crest at line plots, where mako's dark end gets lost; that is a different job from colouring
314
+ thousands of adjacent dies.
315
+
25
316
  ## [0.28.0] — 2026-09-11
26
317
 
27
318
  ### Breaking
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  <img src="docs/images/wafermap-readme-header-256.png" width="64" height="64" alt="wafermap icon">
4
4
 
5
- [![CI](https://github.com/wafertools/wafermap/actions/workflows/ci.yml/badge.svg)](https://github.com/wafertools/wafermap/actions/workflows/ci.yml)
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-~46%20kB-blue)
8
+ ![bundle](https://img.shields.io/badge/data%20layer%20min%2Bgz-~49%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 ~46 kB
16
+ Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~49 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
 
@@ -77,8 +77,8 @@ application built on this library, imports **14** of its ~100 exports. Read the
77
77
  [API reference](https://wafertools.github.io/wafermap/api/) as something to search,
78
78
  not to read.
79
79
 
80
- Note the two import paths. The renderers (`renderWaferMap`, `renderWaferGallery`,
81
- `toCanvas`) live **only** at `@wafertools/wafermap/render`, so importing them from the
80
+ Note the two import paths. The renderers (`renderWaferMap`,
81
+ `renderWaferGallery`) live **only** at `@wafertools/wafermap/render`, so importing them from the
82
82
  root package will fail. That keeps the root entry DOM-free — usable in Node for a
83
83
  build-and-analyse pipeline, and tree-shakeable when you only need the geometry, data
84
84
  and stats layers.
@@ -0,0 +1,10 @@
1
+ import { toCanvas as toCanvasImpl } from './toCanvas.js';
2
+ import { buildDieListSection as buildDieListSectionImpl } from './dieList.js';
3
+ import { openReportModal as openReportModalImpl } from './toolbar.js';
4
+ /** @deprecated Removed in 0.31.0. It belongs to the low-level drawing pipeline, which is being withdrawn: build with `buildWaferMap` and draw with `renderWaferMap` or `renderWaferGallery`. */
5
+ export declare const toCanvas: typeof toCanvasImpl;
6
+ /** @deprecated Removed in 0.31.0. `renderWaferMap` and `renderWaferGallery` show the die list themselves (the `dieList` option). */
7
+ export declare const buildDieListSection: typeof buildDieListSectionImpl;
8
+ /** @deprecated Removed in 0.31.0. Reports open from the Summary panel's report button; route them into your host with `setReportOpener`. */
9
+ export declare const openReportModal: typeof openReportModalImpl;
10
+ //# sourceMappingURL=deprecated.d.ts.map
@@ -0,0 +1 @@
1
+ import{deprecated as e}from"../renderer/deprecate.js";import{toCanvas as t}from"./toCanvas.js";import{buildDieListSection as r}from"./dieList.js";import{openReportModal as i}from"./toolbar.js";const o="If you depend on it, say so at https://github.com/wafertools/wafermap/issues.",a="It belongs to the low-level drawing pipeline, which is being withdrawn: build with buildWaferMap and draw with renderWaferMap or renderWaferGallery.",n="renderWaferMap and renderWaferGallery show the die list themselves (the dieList option).",s="Reports open from the Summary panel's report button; route them into your host with setReportOpener.";export const toCanvas=e(t,"toCanvas",`${a} ${o}`),buildDieListSection=e(r,"buildDieListSection",`${n} ${o}`),openReportModal=e(i,"openReportModal",`${s} ${o}`);
@@ -1,5 +1,5 @@
1
1
  import type { Die } from '../core/dies.js';
2
- import { type Wafer } from '../core/index.js';
2
+ import type { Wafer } from '../core/index.js';
3
3
  import { type MetadataFieldDef, type TestDef } from '../renderer/buildWaferMap.js';
4
4
  import type { WaferMetadata } from '../core/metadata.js';
5
5
  import { type MetadataKeySelection } from '../stats/metadataColumns.js';
@@ -76,8 +76,6 @@ export interface DieListDisplayOptions {
76
76
  * directly into a long document).
77
77
  */
78
78
  maxHeight?: string;
79
- /** CSV filename, including extension. Default: `'dies.csv'`. */
80
- csvFilename?: string;
81
79
  }
82
80
  export interface DieListOptions extends DieListDisplayOptions {
83
81
  /** Heading text. Default: `Die list (N)`. */
@@ -1,13 +1,13 @@
1
- import{hasPosition as R,isPositionedDie as O}from"../core/dies.js";import{classifyDie as z}from"../core/index.js";import{isParametricTest as B,getTestPassStatus as x}from"../renderer/buildWaferMap.js";import{resolveMetadataColumns as V}from"../stats/metadataColumns.js";import{LEADING as D,TRACKING as W,wireControlHover as G,controlStyle as K,SPACE as X,RADIUS as F,FONT as C,CLR as d,saveTextFile as Y}from"./toolbar.js";import{csvField as I}from"./summaryPanel.js";import{fmt as T}from"../renderer/fmt.js";const _=5e4,k=new WeakSet;function Q(n){if(k.has(n))return;k.add(n);const o=n.createElement("style");o.textContent=`
1
+ import{hasPosition as R,isPositionedDie as O}from"../core/dies.js";import{classifyDie as z}from"../core/classify.js";import{isParametricTest as B,getTestPassStatus as x}from"../renderer/buildWaferMap.js";import{resolveMetadataColumns as V}from"../stats/metadataColumns.js";import{LEADING as D,TRACKING as W,wireControlHover as G,controlStyle as K,SPACE as X,RADIUS as I,FONT as C,CLR as c,saveTextFile as Y}from"./toolbar.js";import{csvField as T}from"./summaryPanel.js";import{fmt as k}from"../renderer/fmt.js";const _=5e4,F=new WeakSet;function Q(t){if(F.has(t))return;F.add(t);const o=t.createElement("style");o.textContent=`
2
2
  .wmap-dielist-table { width: 100%; border-collapse: collapse; font-size: 12px; font-variant-numeric: tabular-nums; }
3
3
  .wmap-dielist-th {
4
4
  text-align: left; padding: 4px 8px; position: sticky; top: 0;
5
- background: ${d.panelBg}; color: ${d.label}; font-weight: 600;
6
- border-bottom: 1px solid ${d.menuBorder}; white-space: nowrap;
5
+ background: ${c.panelBg}; color: ${c.label}; font-weight: 600;
6
+ border-bottom: 1px solid ${c.menuBorder}; white-space: nowrap;
7
7
  }
8
8
  .wmap-dielist-td {
9
- padding: 4px 8px; color: ${d.text};
10
- border-bottom: 1px solid ${d.menuBorder}; white-space: nowrap;
9
+ padding: 4px 8px; color: ${c.text};
10
+ border-bottom: 1px solid ${c.menuBorder}; white-space: nowrap;
11
11
  }
12
- `,n.head.appendChild(o)}function c(n,o,t,a){const i=n.createElement(o);return t&&Object.assign(i.style,t),a!==void 0&&(i.textContent=a),i}function U(n){return R(n)?String(n.x):""}function q(n){return R(n)?String(n.y):""}function J(n,o){if(o!==void 0)return o.filter(a=>a.testNumber!==void 0);const t=new Map;for(const a of n)for(const i of Object.keys(a.testValues??{})){const s=Number(i);t.has(s)||t.set(s,{testNumber:s,name:`Test ${s}`})}return[...t.values()].sort((a,i)=>a.testNumber-i.testNumber)}function Z(n,o,t){if(!o)return;const a=new Map;for(const i of n){if(!O(i))continue;const s=o(i);s&&a.set(i,z(i,s,{ringCount:t}))}return a.size>0?a:void 0}export function buildDieListSection(n,o,t={}){if(!n.length)return null;const a=t.ownerDocument??document;Q(a);const i=J(n,o),s=t.maxRows??_,S=Math.max(0,n.length-s),H=S>0?n.slice(0,s):n,m=c(a,"div",{display:"flex",flexDirection:"column",gap:X.md,flex:"1",minHeight:"0",minWidth:"0"}),w=c(a,"div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});w.appendChild(c(a,"div",{fontSize:C.meta,fontWeight:"700",letterSpacing:W,textTransform:"uppercase",color:d.label},t.title??`Die list (${n.length})`));const g=c(a,"button",{fontSize:C.body,padding:"3px 8px",borderRadius:F.control,...K("outlined"),background:d.menuBg},"Export CSV");g.type="button",G(g),w.appendChild(g),m.appendChild(w),t.note&&m.appendChild(c(a,"div",{fontSize:C.body,color:d.label,lineHeight:D.base,flexShrink:"0"},t.note));const v=Z(n,t.getWafer,t.ringCount??4),P=n.some(e=>e.edgeExcluded===!0),j=[...t.extraColumn?[t.extraColumn.label]:[],"X","Y","Ring","Quadrant","Edge excluded","Site","Hard bin","Soft bin",...i.map(e=>e.unit?`${e.name} (${e.unit})`:e.name)],{columns:M,truncatedKeys:h}=V({dies:n,waferMetadata:t.waferMetadata,metadataFields:t.metadataFields,dieKeys:t.metadataColumns,waferKeys:t.waferMetadataColumns==="none"?"none":void 0,waferPlacement:t.waferMetadataColumns??"csv",reservedLabels:j}),y=[...t.extraColumn?[{label:t.extraColumn.label,get:e=>t.extraColumn.get(e)??""}]:[],{label:"X",get:U},{label:"Y",get:q},...v?[{label:"Ring",get:e=>{const l=v.get(e);return l?String(l.ring):""}},{label:"Quadrant",get:e=>v.get(e)?.quadrant??""}]:[],...P?[{label:"Edge excluded",get:e=>e.edgeExcluded?"Yes":"No"}]:[],{label:"Site",get:e=>e.siteNum!==void 0?String(e.siteNum):""},{label:"Hard bin",get:e=>e.hbin!==void 0?String(e.hbin):""},{label:"Soft bin",get:e=>e.sbin!==void 0?String(e.sbin):""},...i.map(e=>{const l=!B(e);return{label:e.unit?`${e.name} (${e.unit})`:e.name,get:r=>{if(l){const p=x(r,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=r.testValues?.[e.testNumber];if(u!==void 0)return T(u,e.unit);const b=x(r,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"},csvGet:r=>{if(l){const p=x(r,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=r.testValues?.[e.testNumber];if(u!==void 0)return T(u,void 0,"engineering");const b=x(r,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"}}}),...M.map(e=>({label:e.label,get:e.get,csvOnly:e.csvOnly}))],N=y.filter(e=>!e.csvOnly),$=c(a,"div",{overflow:"auto",flex:"1",minHeight:"0",minWidth:"0",border:`1px solid ${d.menuBorder}`,borderRadius:F.control,...t.maxHeight?{maxHeight:t.maxHeight}:{}}),f=a.createElement("table");f.className="wmap-dielist-table",f.setAttribute("aria-label",t.title??`Die list (${n.length} dies)`);const E=c(a,"thead"),A=c(a,"tr");for(const e of N){const l=a.createElement("th");l.className="wmap-dielist-th",l.scope="col",l.textContent=e.label,A.appendChild(l)}E.appendChild(A),f.appendChild(E);const L=c(a,"tbody");for(const e of H){const l=c(a,"tr");for(const r of N){const u=a.createElement("td");u.className="wmap-dielist-td",u.textContent=r.get(e),l.appendChild(u)}L.appendChild(l)}if(f.appendChild(L),$.appendChild(f),m.appendChild($),S>0||h.length>0){const e=[];S>0&&e.push(`Showing the first ${s.toLocaleString()} of ${n.length.toLocaleString()} dies. The CSV export contains all ${n.length.toLocaleString()}.`),h.length>0&&e.push(`${h.length} further metadata field${h.length===1?"":"s"} not shown.`),m.appendChild(c(a,"div",{fontSize:C.body,color:d.label,lineHeight:D.base,flexShrink:"0"},e.join(" ")))}return g.addEventListener("click",()=>{const e=[y.map(l=>I(l.label)).join(",")];for(const l of n)e.push(y.map(r=>I((r.csvGet??r.get)(l))).join(","));Y(e.join(`
13
- `),t.csvFilename??"dies.csv","text/csv",t.onSaveText)}),m}
12
+ `,t.head.appendChild(o)}function d(t,o,n,a){const i=t.createElement(o);return n&&Object.assign(i.style,n),a!==void 0&&(i.textContent=a),i}function U(t){return R(t)?String(t.x):""}function q(t){return R(t)?String(t.y):""}function J(t,o){if(o!==void 0)return o.filter(a=>a.testNumber!==void 0);const n=new Map;for(const a of t)for(const i of Object.keys(a.testValues??{})){const s=Number(i);n.has(s)||n.set(s,{testNumber:s,name:`Test ${s}`})}return[...n.values()].sort((a,i)=>a.testNumber-i.testNumber)}function Z(t,o,n){if(!o)return;const a=new Map;for(const i of t){if(!O(i))continue;const s=o(i);s&&a.set(i,z(i,s,{ringCount:n}))}return a.size>0?a:void 0}export function buildDieListSection(t,o,n={}){if(!t.length)return null;const a=n.ownerDocument??document;Q(a);const i=J(t,o),s=n.maxRows??_,S=Math.max(0,t.length-s),H=S>0?t.slice(0,s):t,f=d(a,"div",{display:"flex",flexDirection:"column",gap:X.md,flex:"1",minHeight:"0",minWidth:"0"}),w=d(a,"div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});w.appendChild(d(a,"div",{fontSize:C.meta,fontWeight:"700",letterSpacing:W,textTransform:"uppercase",color:c.label},n.title??`Die list (${t.length})`));const g=d(a,"button",{fontSize:C.body,padding:"3px 8px",borderRadius:I.control,...K("outlined"),background:c.menuBg},"Export CSV");g.type="button",G(g),w.appendChild(g),f.appendChild(w),n.note&&f.appendChild(d(a,"div",{fontSize:C.body,color:c.label,lineHeight:D.base,flexShrink:"0"},n.note));const v=Z(t,n.getWafer,n.ringCount??4),P=t.some(e=>e.edgeExcluded===!0),j=[...n.extraColumn?[n.extraColumn.label]:[],"X","Y","Ring","Quadrant","Edge excluded","Site","Hard bin","Soft bin",...i.map(e=>e.unit?`${e.name} (${e.unit})`:e.name)],{columns:M,truncatedKeys:h}=V({dies:t,waferMetadata:n.waferMetadata,metadataFields:n.metadataFields,dieKeys:n.metadataColumns,waferKeys:n.waferMetadataColumns==="none"?"none":void 0,waferPlacement:n.waferMetadataColumns??"csv",reservedLabels:j}),y=[...n.extraColumn?[{label:n.extraColumn.label,get:e=>n.extraColumn.get(e)??""}]:[],{label:"X",get:U},{label:"Y",get:q},...v?[{label:"Ring",get:e=>{const l=v.get(e);return l?String(l.ring):""}},{label:"Quadrant",get:e=>v.get(e)?.quadrant??""}]:[],...P?[{label:"Edge excluded",get:e=>e.edgeExcluded?"Yes":"No"}]:[],{label:"Site",get:e=>e.siteNum!==void 0?String(e.siteNum):""},{label:"Hard bin",get:e=>e.hbin!==void 0?String(e.hbin):""},{label:"Soft bin",get:e=>e.sbin!==void 0?String(e.sbin):""},...i.map(e=>{const l=!B(e);return{label:e.unit?`${e.name} (${e.unit})`:e.name,get:r=>{if(l){const p=x(r,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=r.testValues?.[e.testNumber];if(u!==void 0)return k(u,e.unit);const b=x(r,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"},csvGet:r=>{if(l){const p=x(r,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=r.testValues?.[e.testNumber];if(u!==void 0)return k(u,void 0,"engineering");const b=x(r,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"}}}),...M.map(e=>({label:e.label,get:e.get,csvOnly:e.csvOnly}))],N=y.filter(e=>!e.csvOnly),$=d(a,"div",{overflow:"auto",flex:"1",minHeight:"0",minWidth:"0",border:`1px solid ${c.menuBorder}`,borderRadius:I.control,...n.maxHeight?{maxHeight:n.maxHeight}:{}}),m=a.createElement("table");m.className="wmap-dielist-table",m.setAttribute("aria-label",n.title??`Die list (${t.length} dies)`);const E=d(a,"thead"),A=d(a,"tr");for(const e of N){const l=a.createElement("th");l.className="wmap-dielist-th",l.scope="col",l.textContent=e.label,A.appendChild(l)}E.appendChild(A),m.appendChild(E);const L=d(a,"tbody");for(const e of H){const l=d(a,"tr");for(const r of N){const u=a.createElement("td");u.className="wmap-dielist-td",u.textContent=r.get(e),l.appendChild(u)}L.appendChild(l)}if(m.appendChild(L),$.appendChild(m),f.appendChild($),S>0||h.length>0){const e=[];S>0&&e.push(`Showing the first ${s.toLocaleString()} of ${t.length.toLocaleString()} dies. The CSV export contains all ${t.length.toLocaleString()}.`),h.length>0&&e.push(`${h.length} further metadata field${h.length===1?"":"s"} not shown.`),f.appendChild(d(a,"div",{fontSize:C.body,color:c.label,lineHeight:D.base,flexShrink:"0"},e.join(" ")))}return g.addEventListener("click",()=>{const e=[y.map(l=>T(l.label)).join(",")];for(const l of t)e.push(y.map(r=>T((r.csvGet??r.get)(l))).join(","));Y(e.join(`
13
+ `),"dies.csv","text/csv",n.onSaveText)}),f}
@@ -1,12 +1,10 @@
1
1
  export { WMAP_VERSION, WMAP_BUILD_TIME } from './version.js';
2
- export { toCanvas } from './toCanvas.js';
3
2
  export type { ToCanvasOptions, ToCanvasResult, HitTarget, ViewportTransform } from './toCanvas.js';
4
3
  export { renderWaferMap } from './renderWaferMap.js';
5
4
  export type { RenderOptions, WaferMapController, WaferViewOptions, WaferPreferences, WaferDisplayState } from './renderWaferMap.js';
6
5
  export { renderWaferGallery } from './renderWaferGallery.js';
7
6
  export type { WaferMapDisplayItem, WaferMapDisplayItemFactory, GalleryOptions, GalleryController } from './renderWaferGallery.js';
8
7
  export type { SummaryPanelOptions, FindingsNotice } from './summaryPanel.js';
9
- export { buildDieListSection } from './dieList.js';
10
8
  export type { DieListOptions, DieListDisplayOptions } from './dieList.js';
11
9
  export { collectWarnings, severityOf } from './warnings.js';
12
10
  export type { WarningsOptions } from './warnings.js';
@@ -16,6 +14,6 @@ export type { DetachWindowOpener } from './toolbar.js';
16
14
  export type { UserGuideExtension } from './toolbar.js';
17
15
  export { openWaferMapGuide } from './toolbar.js';
18
16
  export { ICONS } from './icons.js';
19
- export { openReportModal } from './toolbar.js';
20
17
  export type { OverlayHandle } from './toolbar.js';
18
+ export { toCanvas, buildDieListSection, openReportModal, } from './deprecated.js';
21
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- export{WMAP_VERSION,WMAP_BUILD_TIME}from"./version.js";export{toCanvas}from"./toCanvas.js";export{renderWaferMap}from"./renderWaferMap.js";export{renderWaferGallery}from"./renderWaferGallery.js";export{buildDieListSection}from"./dieList.js";export{collectWarnings,severityOf}from"./warnings.js";export{setDetachWindowOpener}from"./toolbar.js";export{openWaferMapGuide}from"./toolbar.js";export{ICONS}from"./icons.js";export{openReportModal}from"./toolbar.js";
1
+ export{WMAP_VERSION,WMAP_BUILD_TIME}from"./version.js";export{renderWaferMap}from"./renderWaferMap.js";export{renderWaferGallery}from"./renderWaferGallery.js";export{collectWarnings,severityOf}from"./warnings.js";export{setDetachWindowOpener}from"./toolbar.js";export{openWaferMapGuide}from"./toolbar.js";export{ICONS}from"./icons.js";export{toCanvas,buildDieListSection,openReportModal}from"./deprecated.js";
@@ -38,7 +38,6 @@ export interface InsightsTabDeps {
38
38
  * picked up, and so a bin is the same colour in a chart as on the map.
39
39
  */
40
40
  getBinColors: () => BinColors;
41
- passBins: number[];
42
41
  /** Read fresh each render — used by the Overview tab's ring/quadrant regional yield cards. Default 4. */
43
42
  getRingCount?: () => number;
44
43
  onSaveImage?: SaveImageHandler;
@@ -1 +1 @@
1
- import{buildFacetTable as $e,facetValueOf as Be,FACET_NONE_VALUE as De}from"../stats/facets.js";import{mergeTestDefs as Le}from"../stats/mergeTestDefs.js";import{isParametricTest as Ie}from"../renderer/buildWaferMap.js";import{NO_DATA_FILL as ne}from"../renderer/colorMap.js";import{describeWaferPopulation as Oe,populationStat as Ee}from"../stats/population.js";import{LEADING as Pe,ALPHA as Ae,SPACE as w,EDGE_GUTTER as K,FONT as G,CLR as B,RADIUS as Re,SHADOW as je,controlStyle as We,wireControlHover as ae,wireTooltip as Ge}from"./toolbar.js";import{ICONS as He}from"./icons.js";import{renderCapabilityPanel as Ye}from"./charts/capability.js";import{renderBoxplotPanel as Fe}from"./charts/boxplot.js";import{renderTrendPanel as Me}from"./charts/trend.js";import{renderHistogramPanel as Ne}from"./charts/histogram.js";import{renderCorrelationPanel as Ve}from"./charts/correlation.js";import{renderScatterPanel as _e}from"./charts/scatter.js";import{renderBarPanel as oe}from"./charts/barPanel.js";import{renderBinClusterPanel as ze}from"./charts/binCluster.js";import{renderTestPassRatePanel as Ue}from"./charts/testPassRate.js";import{QUANTITY as qe}from"./charts/palette.js";import{cardFrameStyle as Qe,makeChartGridWrap as X,makeLabeledSelect as Ke,makeLinkedGroupSelect as Xe}from"./charts/chartShell.js";import{buildYieldData as N,buildYieldDataCombined as re}from"../stats/yield.js";import{buildBinParetoData as Je}from"../stats/binPareto.js";import{buildLotTestSection as Ze,buildLotFunctionalSection as et,buildMetadataStripBox as tt}from"./summaryPanel.js";import{buildRegionYieldData as ie,buildRingRegions as nt,buildQuadrantRegions as at}from"../stats/regions.js";import{renderRegionYieldDiagram as se}from"./charts/regionYieldDiagram.js";const ot=[{key:"overview",label:"Overview"},{key:"distributions",label:"Distributions"},{key:"correlation",label:"Correlation"}];export function createInsightsTab(D){const{getItems:le,getLotStats:de,getBinColors:J,passBins:T,getRingCount:ce,onSaveImage:v,onSaveText:V,openWafer:H,focusTest:Z}=D,pe=D.showMetadataStrip??!0,R=D.contentInset??K,d=D.ownerDocument??document,E=d.createElement("div");Object.assign(E.style,{display:"none",flexDirection:"column",width:"100%",paddingTop:"0"});const ue=K,P=d.createElement("div");Object.assign(P.style,{paddingLeft:ue});const $=d.createElement("div");Object.assign($.style,{display:"flex",gap:w.xs,alignItems:"center",background:B.menuBg,border:`1px solid ${B.menuBorder}`,borderRadius:Re.container,boxShadow:je.panel,padding:`0 ${w.lg}`,marginBottom:w.lg,marginLeft:R,marginRight:R}),$.setAttribute("role","tablist"),$.addEventListener("keydown",e=>{if(e.key!=="ArrowLeft"&&e.key!=="ArrowRight")return;const n=Array.from($.querySelectorAll('[role="tab"]')),o=n.indexOf(d.activeElement);if(o===-1)return;e.preventDefault();const c=n[(o+(e.key==="ArrowRight"?1:-1)+n.length)%n.length];c.focus(),c.click()});const j=d.createElement("div");Object.assign(j.style,{display:"flex",flexDirection:"column",gap:w.lg,marginTop:"0",paddingLeft:R,paddingRight:R}),E.appendChild(P),E.appendChild($),E.appendChild(j);let W=D.defaultView??"overview",A,I=null,L=null,Y={includeLimits:void 0,clipOutliers:!1};const _=e=>{L!==e&&(L=e,M())};let F=[];function z(e,n,o){H?.(e,n,o)}function fe(e){return H?{open:(n,o)=>z(n,`Wafer ${e.find(c=>c.waferIndex===n)?.label??n}`,o)}:Z&&e.length===1?{open:(n,o)=>Z(o),label:"show this test on the map"}:null}function me(){return le().map((e,n)=>e==null?null:{metadata:e.wafer.metadata??void 0,dies:e.dies,wafer:e.wafer,label:e.label??String(e.wafer.metadata?.waferId??""),waferIndex:n,statsSummary:e.statsSummary,testDefs:e.testDefs}).filter(e=>e!=null)}function ee(e,n){Object.assign(e.style,{background:"none",border:"none",borderBottom:n?`2px solid ${B.iconActive}`:"2px solid transparent",color:n?B.iconActive:B.label,fontWeight:n?"700":"500",fontSize:G.body,padding:`${w.sm} ${K} ${w.md}`,cursor:"pointer",marginBottom:"-1px"})}function be(e,n){const o=d.createElement("button");o.type="button",o.textContent=n;const c=e===W;return ee(o,c),o.dataset.on=c?"true":"false",ae(o,"bare"),o.dataset.wmapInsightsTab=e,o.setAttribute("role","tab"),o.setAttribute("aria-selected",c?"true":"false"),o.tabIndex=c?0:-1,o.addEventListener("click",()=>{W!==e&&(W=e,M())}),o}function ge(e){const n=d.createElement("button");return n.type="button",n.textContent=`\u2039 ${e.label}`,ee(n,!1),n.dataset.wmapInsightsBack="1",n.addEventListener("click",e.onBack),n}function ye(e,n,o,c=!0){const s=X(d),f=J();let y="label";const u=o??"group",m=T.length===1?`bin ${T[0]}`:`bins ${T.join(", ")}`,i=de?.(),b=new Map(i?.lotYieldSeries.map(r=>[r.waferIndex,r.yieldPercent])),h=r=>({...r,label:r.label.replace(/\s*·\s*\d+(\.\d+)?%$/,""),yieldPercent:b.get(r.waferIndex),key:r.waferIndex}),g=e.map(h),p=n?.map(r=>({key:r.key,items:r.items.map(h)})),t=()=>p?re(p,T,y):N(g,T,y),x={title:n?`Yield by ${u} (pass: ${m})`:`Yield by wafer (pass: ${m})`,data:t(),selfControl:{current:y,options:[["yield","Sort: yield"],["label",n?`Sort: ${u}`:"Sort: wafer ID"]],onChange:r=>(y=r,{data:t()})},barColor:()=>qe,valueLabel:r=>`${r.percent.toFixed(1)}%`,reference:r=>{if(r.length<3)return null;const k=r.map(Q=>Q.percent).sort((Q,ke)=>Q-ke),O=k.length%2?k[(k.length-1)/2]:(k[k.length/2-1]+k[k.length/2])/2;return{value:O,label:`median ${O.toFixed(1)}%`}},drill:p?{onOpenGroup:r=>{const k=p.find(O=>O.key===r.label)?.items??[];return{data:N(k,T,y),title:`Yield by wafer \u2014 ${u}: ${r.label} (pass: ${m})`}},onBack:()=>({data:t(),title:`Yield by ${u} (pass: ${m})`}),groupLabelText:u}:void 0,onOpen:H?r=>{typeof r.key=="number"&&z(r.key,`Wafer ${r.label}`)}:void 0,ownerDocument:d};let C=null;if(c&&(C=oe(x,v),C.card.style.minHeight="360px",s.appendChild(C.card)),n){const r=ze({title:"Hard bin pareto",groups:n.map(k=>({key:k.key,items:k.items.map(O=>({dies:O.dies,hardBinCounts:O.statsSummary?.stats.hardBinCounts,softBinCounts:O.statsSummary?.stats.softBinCounts}))})),onSaveImage:v,ownerDocument:d});return r.card.style.minHeight="360px",s.appendChild(r.card),{card:s,destroy:()=>{C?.destroy(),r.destroy()}}}let a="hbin";const l=r=>r===void 0?ne:(a==="hbin"?f.hard:f.soft).get(r)??ne,S=e.map(r=>({dies:r.dies,hardBinCounts:r.statsSummary?.stats.hardBinCounts,softBinCounts:r.statsSummary?.stats.softBinCounts})),te=()=>Je(S,a),ve={title:"Hard bin pareto",data:te(),selfControl:{current:a,options:[["hbin","Hard bins"],["sbin","Soft bins"]],onChange:r=>(a=r,{data:te(),title:`${a==="hbin"?"Hard":"Soft"} bin pareto`})},barColor:r=>l(r.binCode),ownerDocument:d},q=oe(ve,v);return q.card.style.minHeight="360px",s.appendChild(q.card),{card:s,destroy:()=>{C?.destroy(),q.destroy()}}}function U(){const e=d.createElement("div");return e.dataset.wmapChartCard="1",Object.assign(e.style,{...Qe(),display:"flex",flexDirection:"column",gap:w.lg,minWidth:"0"}),e}function he(e){if(P.innerHTML="",P.style.marginBottom="0",!pe)return;const n=tt(e.map(o=>({metadata:o.metadata})),void 0,{facetableOnly:!1});n&&(n.style.marginBottom="0",P.appendChild(n),P.style.marginBottom=w.xs)}function Ce(e,n,o,c){if(!e.length)return{elements:[],testValuesCard:null,functionalCard:null,destroy:()=>{}};const s=e.map(l=>l.wafer),f=e.map(l=>l.dies),y=e.flatMap(l=>l.dies),u=e.map(l=>l.statsSummary),m=u.every(l=>l!==void 0)?u:void 0,i=ce?.()??4,b=[],h=[],p=Ue({groups:(c??[{key:"",items:e}]).map(l=>({key:l.key,items:l.items.map(S=>({dies:S.dies,testSpecYield:S.statsSummary?.stats.testSpecYield,functionalYield:S.statsSummary?.stats.functionalYield}))})),testDefs:o,onSaveImage:v,ownerDocument:d});p.card.style.minHeight="300px",b.push(p.card),h.push(p.destroy);const t=ie(f,s,i,T,nt);if(t.length){const l=se({title:"Ring yield",mode:"ring",rows:t,onSaveImage:v,ownerDocument:d});b.push(l.card),h.push(l.destroy)}const x=ie(f,s,i,T,at);if(x.length){const l=se({title:"Quadrant yield",mode:"quadrant",rows:x,onSaveImage:v,ownerDocument:d});b.push(l.card),h.push(l.destroy)}let C=null;if(n.length){const l=Ze(y,n,void 0,m,V,f.map(S=>({dies:S})),void 0,"full");if(l){const S=U();S.appendChild(l),C=S}}let a=null;if(o.length){const l=et(y,o,m,V);if(l){const S=U();S.appendChild(l),a=S}}return{elements:b,testValuesCard:C,functionalCard:a,destroy:()=>{for(const l of h)l()}}}function xe(e){const n=T.length===1?`bin ${T[0]}`:`bins ${T.join(", ")}`,o=e.length===1,c=e[0],s=U();Object.assign(s.style,{flexDirection:"row",flexWrap:"wrap",gap:w.md});const f=i=>{const b=i.lastElementChild;return b&&Object.assign(b.style,{borderRight:"none",paddingRight:"0",marginRight:"0"}),i};function y(i,b,h){const g=d.createElement("div");Object.assign(g.style,{display:"flex",alignItems:"baseline",gap:w.sm,paddingRight:w.xxl,marginRight:w.md,borderRight:`1px solid ${B.menuBorder}`});const p=d.createElement("div");p.textContent=i,Object.assign(p.style,{fontSize:G.heading,fontWeight:"700",color:B.value,lineHeight:Pe.tight});const t=d.createElement("div");if(t.textContent=b,Object.assign(t.style,{fontSize:G.body,color:B.label}),g.append(p,t),h){const x=d.createElement("div");x.textContent=h,Object.assign(x.style,{fontSize:G.meta,color:B.label,opacity:Ae.muted}),g.appendChild(x)}return g}if(o){const i=N([{...c,key:c.waferIndex}],T)[0]?.percent;return i!==void 0&&s.appendChild(y(`${i.toFixed(1)}%`,`Yield \xB7 pass: ${n}`)),s.appendChild(y(String(c.dies.length),"Total dies")),f(s)}s.appendChild(y(String(e.length),"Wafers"));const u=N(e.map(i=>({...i,key:i.waferIndex})),T).map(i=>i.percent).filter(i=>Number.isFinite(i));if(u.length){const i=u.reduce((g,p)=>g+p,0)/u.length,b=re([{key:"all",items:e.map(g=>({...g,key:g.waferIndex}))}],T)[0]?.percent,h=b!==void 0&&b.toFixed(1)!==i.toFixed(1);s.appendChild(y(`${i.toFixed(1)}%`,`Mean per-wafer yield \xB7 pass: ${n}`,h?`${b.toFixed(1)}% by dies`:void 0))}const m=e.map(i=>i.statsSummary);if(m.every(i=>i!==void 0)){const i=m.reduce((h,g)=>h+g.stats.analyzedDies,0),b=m.reduce((h,g)=>h+g.stats.excludedDies,0);s.appendChild(y(i.toLocaleString(),"Dies analysed",b?`${b.toLocaleString()} excluded`:void 0))}else s.appendChild(y(e.reduce((i,b)=>i+b.dies.length,0).toLocaleString(),"Total dies"));return f(s)}function we(e,n,o,c,s){const f=d.createElement("div");Object.assign(f.style,{display:"flex",flexDirection:"column",gap:w.lg});const y=e.length===1&&!c;e.length&&!c&&f.appendChild(xe(e));const u=ye(e,c,s,!y);f.appendChild(u.card);const m=Ce(e,n,o,c);for(const i of m.elements)u.card.appendChild(i);return m.testValuesCard&&f.appendChild(m.testValuesCard),m.functionalCard&&f.appendChild(m.functionalCard),{card:f,destroy:()=>{u.destroy(),m.destroy()}}}function Se(e,n,o,c){const s=X(d),f=fe(e),y=a=>{Y=a;for(const l of[g,p,t])l!==u&&l?.setAxisPrefs(a)};let u=null;const m=a=>l=>{u=a(),y(l),u=null},i=a=>({...a,testStats:a.statsSummary?.stats.perTestStats}),b=e.map(i),h=o?.map(a=>({key:a.key,items:a.items.map(i)})),g=Fe({title:"Test value distribution",axisPrefs:Y,onAxisPrefsChange:m(()=>g),onTestChange:a=>x(a),selectedTestNumber:I??void 0,items:b,testDefs:n,groups:h,groupLabelText:c,onSaveImage:v,onGroupChange:a=>_(a),onOpen:f?.open,openActionLabel:f?.label,ownerDocument:d}),p=Ne({title:"Value histogram",axisPrefs:Y,onAxisPrefsChange:m(()=>p),onTestChange:a=>x(a),selectedTestNumber:I??void 0,items:e,testDefs:n,groups:o,onGroupChange:a=>_(a),onSaveImage:v,ownerDocument:d}),t=Me({title:"Wafer-to-wafer trend",axisPrefs:Y,onAxisPrefsChange:m(()=>t),onTestChange:a=>x(a),selectedTestNumber:I??void 0,items:e.map(a=>({label:a.label,key:a.waferIndex,dies:a.dies,testStats:a.statsSummary?.stats.perTestStats})),testDefs:n,onSaveImage:v,centreLabel:Ee(Oe(e.map(a=>a.wafer.metadata)),"mean"),onOpen:H?(a,l)=>z(a,`Wafer ${e.find(S=>S.waferIndex===a)?.label??a}`,l):void 0,ownerDocument:d}),x=a=>{I=a,g.setTest(a),p.setTest(a),t.setTest(a),C.setTest(a)},C=Ye({title:"Process capability",items:e,testDefs:n,onSaveImage:v,selectedTestNumber:I??void 0,onSelectTest:a=>x(a),ownerDocument:d});return s.append(C.card,g.card,p.card,t.card),{card:s,destroy:()=>{C.destroy(),g.destroy(),p.destroy(),t.destroy()}}}function Te(e,n,o){const c=X(d),s=_e({title:"Test scatter",items:e,testDefs:n,groups:o,binColors:J().hard,onSaveImage:v,ownerDocument:d}),f=Ve({title:"Test correlation matrix",items:e,testDefs:n,onSaveImage:v,onSaveText:V,onSelectPair:(y,u)=>s.setXY(y,u),ownerDocument:d});return c.append(f.card,s.card),{card:c,destroy:()=>{f.destroy(),s.destroy()}}}function M(){for(const t of F)t.destroy();F=[],j.innerHTML="",$.innerHTML="",D.backTab&&$.appendChild(ge(D.backTab));for(const t of ot)$.appendChild(be(t.key,t.label));const e=me();he(e);const n=$e(e,{facetableOnly:!0}).filter(t=>t.splittable),o=d.createElement("div");Object.assign(o.style,{display:"flex",gap:w.md,alignItems:"center",marginLeft:w.xxl,paddingBottom:w.xs});let c;n.length>0?(o.appendChild(Ke("Group by:",[{value:"",label:"None"},...n.map(t=>({value:t.key,label:`${t.label} (${t.values.length})`}))],A??"",t=>{A=t||void 0,L=null,M()},{hook:"group-by",ownerDocument:d})),c=n.find(t=>t.key===A)?.label):A=void 0;let s;if(A){const t=new Map,x=[];for(const C of e){const a=Be(C.metadata,A)??De;t.has(a)||(t.set(a,[]),x.push(a)),t.get(a).push(C)}s=x.map(C=>({key:C,items:t.get(C)}))}L!==null&&!s?.some(t=>t.key===L)&&(L=null),s&&s.length>0&&o.appendChild(Xe(s.map(t=>t.key),"Show:",L,t=>_(t),{ownerDocument:d}).el);let f=null;if(D.onOpenGuide){const t=d.createElement("button");t.type="button",t.innerHTML=He.help,t.setAttribute("aria-label","User guide"),Object.assign(t.style,{...We("bare"),display:"inline-flex",alignItems:"center",justifyContent:"center",width:"24px",height:"24px",flexShrink:"0"}),ae(t,"bare"),Ge(t,"User guide"),t.addEventListener("click",()=>D.onOpenGuide()),Object.assign(t.style,{marginLeft:"auto"}),f=t}$.appendChild(o),f&&$.appendChild(f);const y=L!==null?s?.find(t=>t.key===L):void 0,u=y?.items??e,m=y?void 0:s,i=Le(u),b=i.defs,h=b.filter(Ie);I!==null&&!h.some(t=>t.testNumber===I)&&(I=null);const g=W==="overview"?we(u,h,b,m,c):W==="distributions"?Se(u,h,m,c):Te(u,h,m),p=i.conflicts.filter(t=>t.excluded);if(p.length>0){const t=d.createElement("div");Object.assign(t.style,{color:B.label,fontSize:G.body,padding:`0 ${R} ${w.sm}`});const x=p.slice(0,6).map(l=>l.testNumber).join(", "),C=p.length>6?`, and ${p.length-6} more`:"",a=p.length===1?"it":"them";t.textContent=`${p.length} test ${p.length===1?"number is":"numbers are"} hidden (${x}${C}) \u2014 the wafers in view describe ${a} differently, so their values cannot be pooled. `+(s&&s.length>0?`Pick one ${c??"group"} under "Show:" to analyse ${a}.`:"Load one test program at a time to analyse them."),t.dataset.wmapWithheldTests=String(p.length),j.appendChild(t)}F.push(g),j.appendChild(g.card)}return{el:E,render:M,destroy:()=>{for(const e of F)e.destroy();E.remove()}}}
1
+ import{buildFacetTable as De,facetValueOf as $e,FACET_NONE_VALUE as Le}from"../stats/facets.js";import{mergeTestDefs as Ee}from"../stats/mergeTestDefs.js";import{isParametricTest as Ie}from"../renderer/buildWaferMap.js";import{waferDisplayLabel as Oe}from"../core/waferLabel.js";import{INPUT_DEFAULT_PASS_BINS as A,itemPassBins as Pe,passBinsLabel as ne}from"../core/passBins.js";import{NO_DATA_FILL as ae}from"../renderer/colorMap.js";import{describeWaferPopulation as Ae,populationStat as Re}from"../stats/population.js";import{LEADING as We,ALPHA as je,SPACE as S,EDGE_GUTTER as K,FONT as G,CLR as k,RADIUS as Ge,SHADOW as He,controlStyle as Ye,wireControlHover as oe,wireTooltip as Fe}from"./toolbar.js";import{ICONS as Ne}from"./icons.js";import{renderCapabilityPanel as Me}from"./charts/capability.js";import{renderBoxplotPanel as Ve}from"./charts/boxplot.js";import{renderTrendPanel as _e}from"./charts/trend.js";import{renderHistogramPanel as Ue}from"./charts/histogram.js";import{renderCorrelationPanel as ze}from"./charts/correlation.js";import{renderScatterPanel as qe}from"./charts/scatter.js";import{renderBarPanel as re}from"./charts/barPanel.js";import{renderBinClusterPanel as Qe}from"./charts/binCluster.js";import{renderTestPassRatePanel as Ke}from"./charts/testPassRate.js";import{QUANTITY as Xe}from"./charts/palette.js";import{cardFrameStyle as Je,makeChartGridWrap as X,makeLabeledSelect as Ze,makeLinkedGroupSelect as et}from"./charts/chartShell.js";import{buildYieldData as M,buildYieldDataCombined as se}from"../stats/yield.js";import{buildBinParetoData as tt}from"../stats/binPareto.js";import{buildLotTestSection as nt,buildLotFunctionalSection as at,buildMetadataStripBox as ot}from"./summaryPanel.js";import{buildRegionYieldData as ie,buildRingRegions as rt,buildQuadrantRegions as st}from"../stats/regions.js";import{renderRegionYieldDiagram as le}from"./charts/regionYieldDiagram.js";const it=[{key:"overview",label:"Overview"},{key:"distributions",label:"Distributions"},{key:"correlation",label:"Correlation"}];export function createInsightsTab(D){const{getItems:de,getLotStats:ce,getBinColors:J,getRingCount:pe,onSaveImage:T,onSaveText:V,openWafer:H,focusTest:Z}=D,ue=D.showMetadataStrip??!0,R=D.contentInset??K,d=D.ownerDocument??document,I=d.createElement("div");Object.assign(I.style,{display:"none",flexDirection:"column",width:"100%",paddingTop:"0"});const fe=K,O=d.createElement("div");Object.assign(O.style,{paddingLeft:fe});const B=d.createElement("div");Object.assign(B.style,{display:"flex",gap:S.xs,alignItems:"center",background:k.menuBg,border:`1px solid ${k.menuBorder}`,borderRadius:Ge.container,boxShadow:He.panel,padding:`0 ${S.lg}`,marginBottom:S.lg,marginLeft:R,marginRight:R}),B.setAttribute("role","tablist"),B.addEventListener("keydown",e=>{if(e.key!=="ArrowLeft"&&e.key!=="ArrowRight")return;const n=Array.from(B.querySelectorAll('[role="tab"]')),o=n.indexOf(d.activeElement);if(o===-1)return;e.preventDefault();const c=n[(o+(e.key==="ArrowRight"?1:-1)+n.length)%n.length];c.focus(),c.click()});const W=d.createElement("div");Object.assign(W.style,{display:"flex",flexDirection:"column",gap:S.lg,marginTop:"0",paddingLeft:R,paddingRight:R}),I.appendChild(O),I.appendChild(B),I.appendChild(W);let j=D.defaultView??"overview",P,L=null,$=null,Y={includeLimits:void 0,clipOutliers:!1};const _=e=>{$!==e&&($=e,N())};let F=[];function U(e,n,o){H?.(e,n,o)}function me(e){return H?{open:(n,o)=>U(n,`Wafer ${e.find(c=>c.waferIndex===n)?.label??n}`,o)}:Z&&e.length===1?{open:(n,o)=>Z(o),label:"show this test on the map"}:null}function be(){return de().map((e,n)=>e==null?null:{metadata:e.wafer.metadata??void 0,dies:e.dies,wafer:e.wafer,label:Oe(e,n),waferIndex:n,passBins:Pe(e),statsSummary:e.statsSummary,testDefs:e.testDefs}).filter(e=>e!=null)}function ee(e,n){Object.assign(e.style,{background:"none",border:"none",borderBottom:n?`2px solid ${k.iconActive}`:"2px solid transparent",color:n?k.iconActive:k.label,fontWeight:n?"700":"500",fontSize:G.body,padding:`${S.sm} ${K} ${S.md}`,cursor:"pointer",marginBottom:"-1px"})}function ge(e,n){const o=d.createElement("button");o.type="button",o.textContent=n;const c=e===j;return ee(o,c),o.dataset.on=c?"true":"false",oe(o,"bare"),o.dataset.wmapInsightsTab=e,o.setAttribute("role","tab"),o.setAttribute("aria-selected",c?"true":"false"),o.tabIndex=c?0:-1,o.addEventListener("click",()=>{j!==e&&(j=e,N())}),o}function ye(e){const n=d.createElement("button");return n.type="button",n.textContent=`\u2039 ${e.label}`,ee(n,!1),n.dataset.wmapInsightsBack="1",n.addEventListener("click",e.onBack),n}function he(e,n,o,c=!0){const l=X(d),f=J();let y="label";const u=o??"group",m=ne(e.map(s=>s.passBins)),i=ce?.(),b=new Map(i?.lotYieldSeries.map(s=>[s.waferIndex,s.yieldPercent])),h=s=>({...s,label:s.label.replace(/\s*·\s*\d+(\.\d+)?%$/,""),yieldPercent:b.get(s.waferIndex),key:s.waferIndex}),g=e.map(h),p=n?.map(s=>({key:s.key,items:s.items.map(h)})),t=()=>p?se(p,A,y):M(g,A,y),x={title:n?`Yield by ${u} (pass: ${m})`:`Yield by wafer (pass: ${m})`,data:t(),selfControl:{current:y,options:[["yield","Sort: yield"],["label",n?`Sort: ${u}`:"Sort: wafer ID"]],onChange:s=>(y=s,{data:t()})},barColor:()=>Xe,valueLabel:s=>`${s.percent.toFixed(1)}%`,reference:s=>{if(s.length<3)return null;const v=s.map(Q=>Q.percent).sort((Q,ke)=>Q-ke),E=v.length%2?v[(v.length-1)/2]:(v[v.length/2-1]+v[v.length/2])/2;return{value:E,label:`median ${E.toFixed(1)}%`}},drill:p?{onOpenGroup:s=>{const v=p.find(E=>E.key===s.label)?.items??[];return{data:M(v,A,y),title:`Yield by wafer \u2014 ${u}: ${s.label} (pass: ${m})`}},onBack:()=>({data:t(),title:`Yield by ${u} (pass: ${m})`}),groupLabelText:u}:void 0,onOpen:H?s=>{typeof s.key=="number"&&U(s.key,`Wafer ${s.label}`)}:void 0,ownerDocument:d};let C=null;if(c&&(C=re(x,T),C.card.style.minHeight="360px",l.appendChild(C.card)),n){const s=Qe({title:"Hard bin pareto",groups:n.map(v=>({key:v.key,items:v.items.map(E=>({dies:E.dies,hardBinCounts:E.statsSummary?.stats.hardBinCounts,softBinCounts:E.statsSummary?.stats.softBinCounts}))})),onSaveImage:T,ownerDocument:d});return s.card.style.minHeight="360px",l.appendChild(s.card),{card:l,destroy:()=>{C?.destroy(),s.destroy()}}}let a="hbin";const r=s=>s===void 0?ae:(a==="hbin"?f.hard:f.soft).get(s)??ae,w=e.map(s=>({dies:s.dies,hardBinCounts:s.statsSummary?.stats.hardBinCounts,softBinCounts:s.statsSummary?.stats.softBinCounts})),te=()=>tt(w,a),Be={title:"Hard bin pareto",data:te(),selfControl:{current:a,options:[["hbin","Hard bins"],["sbin","Soft bins"]],onChange:s=>(a=s,{data:te(),title:`${a==="hbin"?"Hard":"Soft"} bin pareto`})},barColor:s=>r(s.binCode),ownerDocument:d},q=re(Be,T);return q.card.style.minHeight="360px",l.appendChild(q.card),{card:l,destroy:()=>{C?.destroy(),q.destroy()}}}function z(){const e=d.createElement("div");return e.dataset.wmapChartCard="1",Object.assign(e.style,{...Je(),display:"flex",flexDirection:"column",gap:S.lg,minWidth:"0"}),e}function Ce(e){if(O.innerHTML="",O.style.marginBottom="0",!ue)return;const n=ot(e.map(o=>({metadata:o.metadata})),void 0,{facetableOnly:!1});n&&(n.style.marginBottom="0",O.appendChild(n),O.style.marginBottom=S.xs)}function xe(e,n,o,c){if(!e.length)return{elements:[],testValuesCard:null,functionalCard:null,destroy:()=>{}};const l=e.map(r=>r.wafer),f=e.map(r=>r.dies),y=e.flatMap(r=>r.dies),u=e.map(r=>r.statsSummary),m=u.every(r=>r!==void 0)?u:void 0,i=pe?.()??4,b=[],h=[],p=Ke({groups:(c??[{key:"",items:e}]).map(r=>({key:r.key,items:r.items.map(w=>({dies:w.dies,testSpecYield:w.statsSummary?.stats.testSpecYield,functionalYield:w.statsSummary?.stats.functionalYield}))})),testDefs:o,onSaveImage:T,ownerDocument:d});p.card.style.minHeight="300px",b.push(p.card),h.push(p.destroy);const t=ie(f,l,i,r=>e[r].passBins,rt);if(t.length){const r=le({title:"Ring yield",mode:"ring",rows:t,onSaveImage:T,ownerDocument:d});b.push(r.card),h.push(r.destroy)}const x=ie(f,l,i,r=>e[r].passBins,st);if(x.length){const r=le({title:"Quadrant yield",mode:"quadrant",rows:x,onSaveImage:T,ownerDocument:d});b.push(r.card),h.push(r.destroy)}let C=null;if(n.length){const r=nt(y,n,void 0,m,V,f.map(w=>({dies:w})),void 0,"full");if(r){const w=z();w.appendChild(r),C=w}}let a=null;if(o.length){const r=at(y,o,m,V);if(r){const w=z();w.appendChild(r),a=w}}return{elements:b,testValuesCard:C,functionalCard:a,destroy:()=>{for(const r of h)r()}}}function Se(e){const n=ne(e.map(i=>i.passBins)),o=e.length===1,c=e[0],l=z();Object.assign(l.style,{flexDirection:"row",flexWrap:"wrap",gap:S.md});const f=i=>{const b=i.lastElementChild;return b&&Object.assign(b.style,{borderRight:"none",paddingRight:"0",marginRight:"0"}),i};function y(i,b,h){const g=d.createElement("div");Object.assign(g.style,{display:"flex",alignItems:"baseline",gap:S.sm,paddingRight:S.xxl,marginRight:S.md,borderRight:`1px solid ${k.menuBorder}`});const p=d.createElement("div");p.textContent=i,Object.assign(p.style,{fontSize:G.heading,fontWeight:"700",color:k.value,lineHeight:We.tight});const t=d.createElement("div");if(t.textContent=b,Object.assign(t.style,{fontSize:G.body,color:k.label}),g.append(p,t),h){const x=d.createElement("div");x.textContent=h,Object.assign(x.style,{fontSize:G.meta,color:k.label,opacity:je.muted}),g.appendChild(x)}return g}if(o){const i=M([{...c,key:c.waferIndex}],A)[0]?.percent;return i!==void 0&&l.appendChild(y(`${i.toFixed(1)}%`,`Yield \xB7 pass: ${n}`)),l.appendChild(y(String(c.dies.length),"Total dies")),f(l)}l.appendChild(y(String(e.length),"Wafers"));const u=M(e.map(i=>({...i,key:i.waferIndex})),A).map(i=>i.percent).filter(i=>Number.isFinite(i));if(u.length){const i=u.reduce((g,p)=>g+p,0)/u.length,b=se([{key:"all",items:e.map(g=>({...g,key:g.waferIndex}))}],A)[0]?.percent,h=b!==void 0&&b.toFixed(1)!==i.toFixed(1);l.appendChild(y(`${i.toFixed(1)}%`,`Mean per-wafer yield \xB7 pass: ${n}`,h?`${b.toFixed(1)}% by dies`:void 0))}const m=e.map(i=>i.statsSummary);if(m.every(i=>i!==void 0)){const i=m.reduce((h,g)=>h+g.stats.analyzedDies,0),b=m.reduce((h,g)=>h+g.stats.excludedDies,0);l.appendChild(y(i.toLocaleString(),"Dies analysed",b?`${b.toLocaleString()} excluded`:void 0))}else l.appendChild(y(e.reduce((i,b)=>i+b.dies.length,0).toLocaleString(),"Total dies"));return f(l)}function we(e,n,o,c,l){const f=d.createElement("div");Object.assign(f.style,{display:"flex",flexDirection:"column",gap:S.lg});const y=e.length===1&&!c;e.length&&!c&&f.appendChild(Se(e));const u=he(e,c,l,!y);f.appendChild(u.card);const m=xe(e,n,o,c);for(const i of m.elements)u.card.appendChild(i);return m.testValuesCard&&f.appendChild(m.testValuesCard),m.functionalCard&&f.appendChild(m.functionalCard),{card:f,destroy:()=>{u.destroy(),m.destroy()}}}function Te(e,n,o,c){const l=X(d),f=me(e),y=a=>{Y=a;for(const r of[g,p,t])r!==u&&r?.setAxisPrefs(a)};let u=null;const m=a=>r=>{u=a(),y(r),u=null},i=a=>({...a,testStats:a.statsSummary?.stats.perTestStats}),b=e.map(i),h=o?.map(a=>({key:a.key,items:a.items.map(i)})),g=Ve({title:"Test value distribution",axisPrefs:Y,onAxisPrefsChange:m(()=>g),onTestChange:a=>x(a),selectedTestNumber:L??void 0,items:b,testDefs:n,groups:h,groupLabelText:c,onSaveImage:T,onGroupChange:a=>_(a),onOpen:f?.open,openActionLabel:f?.label,ownerDocument:d}),p=Ue({title:"Value histogram",axisPrefs:Y,onAxisPrefsChange:m(()=>p),onTestChange:a=>x(a),selectedTestNumber:L??void 0,items:e,testDefs:n,groups:o,onGroupChange:a=>_(a),onSaveImage:T,ownerDocument:d}),t=_e({title:"Wafer-to-wafer trend",axisPrefs:Y,onAxisPrefsChange:m(()=>t),onTestChange:a=>x(a),selectedTestNumber:L??void 0,items:e.map(a=>({label:a.label,key:a.waferIndex,dies:a.dies,testStats:a.statsSummary?.stats.perTestStats})),testDefs:n,onSaveImage:T,centreLabel:Re(Ae(e.map(a=>a.wafer.metadata)),"mean"),onOpen:H?(a,r)=>U(a,`Wafer ${e.find(w=>w.waferIndex===a)?.label??a}`,r):void 0,ownerDocument:d}),x=a=>{L=a,g.setTest(a),p.setTest(a),t.setTest(a),C.setTest(a)},C=Me({title:"Process capability",items:e,testDefs:n,onSaveImage:T,selectedTestNumber:L??void 0,onSelectTest:a=>x(a),ownerDocument:d});return l.append(C.card,g.card,p.card,t.card),{card:l,destroy:()=>{C.destroy(),g.destroy(),p.destroy(),t.destroy()}}}function ve(e,n,o){const c=X(d),l=qe({title:"Test scatter",items:e,testDefs:n,groups:o,binColors:J().hard,onSaveImage:T,ownerDocument:d}),f=ze({title:"Test correlation matrix",items:e,testDefs:n,onSaveImage:T,onSaveText:V,onSelectPair:(y,u)=>l.setXY(y,u),ownerDocument:d});return c.append(f.card,l.card),{card:c,destroy:()=>{f.destroy(),l.destroy()}}}function N(){for(const t of F)t.destroy();F=[],W.innerHTML="",B.innerHTML="",D.backTab&&B.appendChild(ye(D.backTab));for(const t of it)B.appendChild(ge(t.key,t.label));const e=be();Ce(e);const n=De(e,{facetableOnly:!0}).filter(t=>t.splittable),o=d.createElement("div");Object.assign(o.style,{display:"flex",gap:S.md,alignItems:"center",marginLeft:S.xxl,paddingBottom:S.xs});let c;n.length>0?(o.appendChild(Ze("Group by:",[{value:"",label:"None"},...n.map(t=>({value:t.key,label:`${t.label} (${t.values.length})`}))],P??"",t=>{P=t||void 0,$=null,N()},{hook:"group-by",ownerDocument:d})),c=n.find(t=>t.key===P)?.label):P=void 0;let l;if(P){const t=new Map,x=[];for(const C of e){const a=$e(C.metadata,P)??Le;t.has(a)||(t.set(a,[]),x.push(a)),t.get(a).push(C)}l=x.map(C=>({key:C,items:t.get(C)}))}$!==null&&!l?.some(t=>t.key===$)&&($=null),l&&l.length>0&&o.appendChild(et(l.map(t=>t.key),"Show:",$,t=>_(t),{ownerDocument:d}).el);let f=null;if(D.onOpenGuide){const t=d.createElement("button");t.type="button",t.innerHTML=Ne.help,t.setAttribute("aria-label","User guide"),Object.assign(t.style,{...Ye("bare"),display:"inline-flex",alignItems:"center",justifyContent:"center",width:"24px",height:"24px",flexShrink:"0"}),oe(t,"bare"),Fe(t,"User guide"),t.addEventListener("click",()=>D.onOpenGuide()),Object.assign(t.style,{marginLeft:"auto"}),f=t}B.appendChild(o),f&&B.appendChild(f);const y=$!==null?l?.find(t=>t.key===$):void 0,u=y?.items??e,m=y?void 0:l,i=Ee(u),b=i.defs,h=b.filter(Ie);L!==null&&!h.some(t=>t.testNumber===L)&&(L=null);const g=j==="overview"?we(u,h,b,m,c):j==="distributions"?Te(u,h,m,c):ve(u,h,m),p=i.conflicts.filter(t=>t.excluded);if(p.length>0){const t=d.createElement("div");Object.assign(t.style,{color:k.label,fontSize:G.body,padding:`0 ${R} ${S.sm}`});const x=p.slice(0,6).map(r=>r.testNumber).join(", "),C=p.length>6?`, and ${p.length-6} more`:"",a=p.length===1?"it":"them";t.textContent=`${p.length} test ${p.length===1?"number is":"numbers are"} hidden (${x}${C}) \u2014 the wafers in view describe ${a} differently, so their values cannot be pooled. `+(l&&l.length>0?`Pick one ${c??"group"} under "Show:" to analyse ${a}.`:"Load one test program at a time to analyse them."),t.dataset.wmapWithheldTests=String(p.length),W.appendChild(t)}F.push(g),W.appendChild(g.card)}return{el:I,render:N,destroy:()=>{for(const e of F)e.destroy();I.remove()}}}