@wafertools/wafermap 0.29.0 → 0.30.1

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 (88) hide show
  1. package/AGENTS.md +26 -17
  2. package/CHANGELOG.md +410 -0
  3. package/README.md +6 -6
  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/correlation.js +2 -2
  9. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +1 -0
  10. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  13. package/dist/packages/canvas-adapter/deprecated.d.ts +7 -0
  14. package/dist/packages/canvas-adapter/deprecated.js +1 -0
  15. package/dist/packages/canvas-adapter/dieList.d.ts +1 -3
  16. package/dist/packages/canvas-adapter/dieList.js +7 -7
  17. package/dist/packages/canvas-adapter/exportName.d.ts +68 -0
  18. package/dist/packages/canvas-adapter/exportName.js +1 -0
  19. package/dist/packages/canvas-adapter/icons.js +2 -2
  20. package/dist/packages/canvas-adapter/index.d.ts +2 -3
  21. package/dist/packages/canvas-adapter/index.js +1 -1
  22. package/dist/packages/canvas-adapter/insightsTab.d.ts +0 -1
  23. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  24. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  25. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +23 -40
  26. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  27. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +96 -104
  28. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  29. package/dist/packages/canvas-adapter/summaryPanel.d.ts +31 -13
  30. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  31. package/dist/packages/canvas-adapter/toCanvas.d.ts +20 -12
  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 +26 -15
  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/deprecated.d.ts +60 -0
  41. package/dist/packages/core/deprecated.js +1 -0
  42. package/dist/packages/core/index.d.ts +1 -3
  43. package/dist/packages/core/index.js +1 -1
  44. package/dist/packages/core/passBins.d.ts +33 -0
  45. package/dist/packages/core/passBins.js +1 -0
  46. package/dist/packages/core/utils.d.ts +12 -0
  47. package/dist/packages/core/utils.js +1 -1
  48. package/dist/packages/core/waferLabel.d.ts +35 -0
  49. package/dist/packages/core/waferLabel.js +1 -0
  50. package/dist/packages/renderer/binColors.d.ts +105 -13
  51. package/dist/packages/renderer/binColors.js +1 -1
  52. package/dist/packages/renderer/buildView.d.ts +8 -9
  53. package/dist/packages/renderer/buildView.js +1 -1
  54. package/dist/packages/renderer/buildWaferMap.d.ts +76 -17
  55. package/dist/packages/renderer/buildWaferMap.js +1 -1
  56. package/dist/packages/renderer/colorMap.d.ts +4 -3
  57. package/dist/packages/renderer/colorSchemes.d.ts +8 -6
  58. package/dist/packages/renderer/deprecate.d.ts +31 -0
  59. package/dist/packages/renderer/deprecate.js +1 -0
  60. package/dist/packages/renderer/deprecated.d.ts +45 -0
  61. package/dist/packages/renderer/deprecated.js +1 -0
  62. package/dist/packages/renderer/index.d.ts +4 -4
  63. package/dist/packages/renderer/index.js +1 -1
  64. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  65. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  66. package/dist/packages/stats/binPareto.d.ts +6 -1
  67. package/dist/packages/stats/capability.d.ts +16 -1
  68. package/dist/packages/stats/capability.js +1 -1
  69. package/dist/packages/stats/clusterDetection.d.ts +0 -2
  70. package/dist/packages/stats/deprecated.d.ts +82 -0
  71. package/dist/packages/stats/deprecated.js +1 -0
  72. package/dist/packages/stats/index.d.ts +5 -14
  73. package/dist/packages/stats/index.js +1 -1
  74. package/dist/packages/stats/normalizeInput.d.ts +10 -0
  75. package/dist/packages/stats/normalizeInput.js +1 -0
  76. package/dist/packages/stats/regions.d.ts +9 -2
  77. package/dist/packages/stats/regions.js +1 -1
  78. package/dist/packages/stats/renderFindingsReport.js +1 -1
  79. package/dist/packages/stats/renderSummaryReport.d.ts +42 -1
  80. package/dist/packages/stats/renderSummaryReport.js +34 -34
  81. package/dist/packages/stats/reportHtml.d.ts +0 -1
  82. package/dist/packages/stats/reportHtml.js +10 -10
  83. package/dist/packages/stats/summaryFigures.d.ts +47 -0
  84. package/dist/packages/stats/summaryFigures.js +1 -0
  85. package/dist/packages/stats/types.d.ts +118 -43
  86. package/dist/packages/stats/yield.d.ts +12 -6
  87. package/dist/packages/stats/yield.js +1 -1
  88. 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,12 +80,13 @@ 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
90
- schemes: `binColorScheme` and `valueColorScheme`.
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
87
+ schemes: `binColorScheme` and `valueColorScheme`. Need the colours for a surface of
88
+ your own (a table swatch, an export)? Read `controller.getBinColors()` for a live map,
89
+ or `binColorsForMaps(results)` — never a palette lookup of your own.
91
90
  - Build once, render many: `buildWaferMap()` handles data + geometry; re-render UI
92
91
  changes through the controller's `setOptions()`, not by rebuilding.
93
92
  - `result.view` is internal. Use the promoted fields: `result.plotMode`,
@@ -99,8 +98,8 @@ correctness guarantee that comes with it.
99
98
  `w.message` to display, branch on `w.code`. Code that calls a string method on an
100
99
  entry — `warnings[0].includes('…')` — is the old shape and will throw.
101
100
  - **`summary.findings` is the complete list and contains restatements of the same
102
- fact.** Building a list for a human to read? Exclude what other findings absorb:
103
- `const absorbed = new Set(summary.findings.flatMap(f => f.absorbedIds ?? []))`.
101
+ fact.** Building a list for a human to read? Pass it through `visibleFindings()`,
102
+ which drops what other findings absorb — do not re-implement that filter.
104
103
  Skip that and one edge failure is reported up to three times per region — a hard
105
104
  bin row, its soft-bin twin, and the yield row that restates the pass bin. Do NOT
106
105
  use `relatedIds` for this; it is a different relationship and some ids it names
@@ -136,6 +135,14 @@ it is handed, because it has no way to know which tests anyone will look at.
136
135
  - **A Web Worker buys responsiveness, not speed.** `createWafermapWorker` copies data
137
136
  across `postMessage`, so total time goes *up*. Use it when a build would otherwise
138
137
  visibly freeze the page, not for small datasets.
138
+ - **Capability, pass rates and region yield come back from the analysis** —
139
+ `stats.capability` (with `computePerTestStats`), `stats.testSpecYield`,
140
+ `stats.testFlagYield`, `stats.functionalYield` and `stats.regionYield`, on wafer and
141
+ lot summaries alike. Do not compute Cp/Cpk or ring yield yourself: the pooled
142
+ within-wafer stddev and per-wafer pass bins are easy to get subtly wrong.
143
+ - **Reports from code: `renderWaferReportHtml(result, summary)` and
144
+ `renderLotReportHtml(results)`** — they take the built maps, so pass bins and ring
145
+ count cannot be wrong. They run in Node.
139
146
  - **In a gallery, pass `perWaferSummaries` to `analyzeWaferLot`** so it reuses the
140
147
  per-wafer analysis you already ran instead of redoing it.
141
148
 
@@ -146,6 +153,8 @@ it is handed, because it has no way to know which tests anyone will look at.
146
153
  | `DieResult.values` / `Die.values` | `testValues` (keyed by test number) |
147
154
  | `TestDef.index` | `TestDef.testNumber` (required) |
148
155
  | `ViewOptions.colorBySpec` | `passFailDisplay: 'spec'` |
156
+ | `View.colorBySpec` | `view.passFailDisplay` |
157
+ | `WaferMapResult.inference.warnings` | `WaferMapResult.warnings` (structured, with a `code`) |
149
158
  | `ViewOptions.testIndex` | `activeTest` |
150
159
  | `mountWaferCanvas` | `renderWaferMap` |
151
160
  | `HARD_BIN_COLORS` / `SOFT_BIN_COLORS` | `BIN_PALETTE` |
@@ -153,15 +162,15 @@ it is handed, because it has no way to know which tests anyone will look at.
153
162
  | `MountOptions` | `RenderOptions` |
154
163
  | `WaferCanvasController` | `WaferMapController` |
155
164
  | `CanvasHitTarget` | `HitTarget` |
156
- | `buildScene` / `BuildSceneOptions` / `SceneOptions` | `buildView` / `ViewOptions` |
165
+ | `buildScene` / `BuildSceneOptions` / `SceneOptions` | `renderWaferMap` |
157
166
  | `WaferFlat`, field `flat` | `WaferNotch`, field `notch` |
158
- | `isInsideWaferWithFlat` | `isInsideWafer` |
167
+ | `isInsideWaferWithFlat` | nothing — `buildWaferMap` resolves the geometry |
159
168
  | `DieSample` / `WaferMapPoint` | `DieResult` |
160
169
  | `colorScheme` / `WaferViewOptions.colorScheme` | `binColorScheme` (bin maps) and `valueColorScheme` (value and stacked maps) |
161
170
  | `registerColorScheme` / `getColorScheme` / `listColorSchemes` | `registerBinColorScheme` / `registerValueColorScheme` and their `get` / `list` pairs |
162
- | `hardBinColor` / `softBinColor` / `hardBinGreyscale` | `resolveBinColors`, or `View.binColors` from a rendered map |
171
+ | `hardBinColor` / `softBinColor` / `hardBinGreyscale` | `BinDef.color`, or `registerBinColorScheme` |
163
172
  | `plotMode: 'specLimit'` | `passFailDisplay: 'spec'` |
164
- | standalone `getDieAtPoint` | `hitTarget.getDieAtPoint` from `toCanvas()` |
173
+ | standalone `getDieAtPoint` | `onHover` / `onClick` on `renderWaferMap` |
165
174
  | `RenderOptions.tooltipTestLimit` | (was a no-op; nothing replaces it) |
166
175
 
167
176
  Passing a removed option is a type error, and is ignored at runtime. Do not add
package/CHANGELOG.md CHANGED
@@ -22,6 +22,416 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
25
+ ## [0.30.1] — 2026-09-16
26
+
27
+ ### Security
28
+
29
+ - **Names from a data file could run script in a tooltip.** The die tooltip and the Insights
30
+ chart tooltips (correlation, capability, boxplot, trend, histogram, bar and grouped-bar
31
+ charts), plus the legend-row and toolbar tooltips, built HTML from test names, units, bin
32
+ names, wafer labels and metadata values without escaping them. A file with a test named
33
+ `<img src=x onerror=…>` ran that script when a user hovered, in a Tauri, Electron or
34
+ WebView2 host inside the app's own webview. Every such value is now escaped. The escape
35
+ helper, formerly private to the reports, is the one copy for the library (`core/utils.ts`),
36
+ and `tests/htmlEscaping.test.mjs` fails if a tooltip puts a raw label into HTML again.
37
+
38
+ ### Changed
39
+
40
+ - **Every saved file is named for the data it came from.** Exports were named for their content
41
+ alone — `dies.csv`, `test-values.csv`, `wafermap.png`, `wafer-gallery.png`, a chart's title — so
42
+ the same export from two wafers of a lot saved as `dies.csv` and `dies (1).csv`, with nothing in
43
+ either name to say which wafer it described. Map, gallery and chart PNGs and every CSV export
44
+ are now named `[lot]_[wafer]_[content]`, for example `LOT123_W05_hard-bin.png`,
45
+ `LOT123_W05_die-list.csv` and `LOT123_25-wafers_yield-by-wafer.png` (see API §5.4.5).
46
+ - A part the data doesn't have is left out, never invented. A wafer with no `label` or
47
+ `waferId` gets no wafer part, not a position that could be read as an ID.
48
+ - A gallery writes its lot and wafer counts (`2-lots`, `25-wafers`), and a lot-stacked map
49
+ writes `stacked-N-wafers`. A card or detached window names files for its own wafer.
50
+ - The name is worked out at save time, so it follows `setResult`, `setItems` and plot-mode
51
+ changes.
52
+ - Names are safe on every common filesystem: path and reserved characters are replaced,
53
+ length is limited, and Windows device names are avoided.
54
+ - **`downloadFilename` is unchanged.** When a host sets it, the map or gallery PNG is still
55
+ named `<downloadFilename>.png`. Only its defaults change: without it, the map PNG was
56
+ `wafermap.png` and the gallery PNG `wafer-gallery.png`, and both now get the generated name.
57
+ Gallery cards, which never read it, also get the generated name, as do all CSVs and charts.
58
+ - `onSaveImage` and `onSaveText` receive the generated name as `suggestedName`. A host that
59
+ matched one of the old default names needs to change.
60
+ - The die-list export's content part is `die-list` (was `dies`).
61
+ - **A floating window's minimize button is now Collapse, and is hidden while the window is
62
+ maximized.** In a desktop host such as Tauri, where a detached gallery card falls back to an
63
+ in-page window, a maximized window's header sits directly under the app's own title bar. Its
64
+ `_` minimize looked like the OS button beside it but only shrank the window to a title strip.
65
+ The button is now drawn as chevrons, with the tooltip **Collapse** (**Show contents** while
66
+ collapsed), and a maximized window shows only restore and close, like a modal. This applies to
67
+ the detached-card fallback window and the user guide window. Modals are unchanged, and
68
+ `ICONS.windowMinimize`/`windowRestore` are still exported.
69
+ - **The data layer is about 3 KB larger (~52 KB gzip).** Analysis now returns capability, pass
70
+ rates by recorded verdict and region yield (see Added), so it includes the code that computes
71
+ them.
72
+
73
+ ### Deprecated
74
+
75
+ - **`downloadFilename` (on `RenderOptions` and `GalleryOptions`) becomes a prefix in 0.31.0.** It
76
+ will lead the name of every file a map or gallery saves, CSVs included, with the lot, wafer and
77
+ content appended, for example `LOT123_sort_W05_hard-bin.png`. Parts it already names won't be
78
+ repeated, so a host passing a source-file stem that contains the lot won't see the lot twice.
79
+ It is not being removed. Until 0.31.0 it keeps its current meaning, and passing it logs a
80
+ one-time console notice. If you rely on setting a map's whole file name, say so at
81
+ https://github.com/wafertools/wafermap/issues.
82
+ - **Five 0.30.0 deprecations are withdrawn on review**, as each is the only supported path to
83
+ something a host needs: `visibleFindings` (the one rule for collapsing restated findings —
84
+ `summary.findings` is deliberately uncollapsed), `openReportModal` (shows a report when a host
85
+ has installed its own `setReportOpener`), `metadataDisplayValue` (the one rule for a metadata
86
+ value's text in an export), `getReticleCell` (the only source of a die's reticle cell outside
87
+ the tooltip) and `renderFindingsReportHtml` (it takes a summary alone, so it cannot be given
88
+ inconsistent inputs). They log no notice and stay.
89
+ - **Every remaining deprecation notice now names what to use instead**, or says plainly that
90
+ there is no data replacement (the histogram, scatter and correlation builders). The review
91
+ behind each verdict is recorded in the repository's `API_REMOVALS.md`.
92
+
93
+ ### Fixed
94
+
95
+ - **A test could not be chosen from the plot-mode menu on a touchscreen.** With more tests than
96
+ fit inline, "Test Value ▶" opens a submenu, and it opened on hover only. On a phone or tablet a
97
+ tap opened the submenu and the same tap closed it again, so the list flashed and no test could
98
+ be picked. From the keyboard, Enter on the row did nothing. The row now also opens the submenu
99
+ when tapped or activated with Enter or Space. Focus moves into the submenu when it is opened from
100
+ the keyboard, the arrow keys move through its tests, and Escape closes it. Tapping the submenu's
101
+ filter box no longer closes the menu underneath. Applies to both `renderWaferMap` and
102
+ `renderWaferGallery`. Guarded by `tests/modeMenuCascade.test.mjs`.
103
+ - **Map lines and markers got thinner as display scaling rose.** Ring, quadrant and reticle
104
+ lines, the wafer outline, the probe path, the +X/+Y indicator, out-of-spec triangles and the
105
+ failing-die hatch were sized in device pixels, so on a 2× display they drew at half their
106
+ intended size and on a 3× display at a third. The ring and quadrant lines' light centre fell
107
+ below one CSS pixel and faded to a faint hairline. They are now sized in CSS pixels, like the
108
+ colorbar's limit markers, so they look the same at every scale and are sharper at higher ones.
109
+ A 1× display draws exactly as before. The die outline stays at its device-pixel width, so
110
+ dense maps on high-DPI screens are not greyed out. Guarded by `tests/strokeScale.test.mjs`.
111
+ - **Outlined buttons lost their border in a dark theme.** Summary report, View die list and the
112
+ Test values / Functional CSV buttons drew a fixed dark edge (`rgba(0,0,0,0.30)`) whatever the
113
+ theme, so on a dark panel they had no visible outline while the ring/quadrant toggles beside
114
+ them did. `--wmap-control-border` now falls back to `--wmap-border` — the edge the
115
+ toggles use — so a theme that sets `--wmap-border` gets the same outline on both. With neither
116
+ set the default is unchanged, and a host that sets `--wmap-control-border` is unaffected.
117
+ - **Bins and test values given as text built a wrong map without a warning.** A CSV parser gives
118
+ every field as a string. String `x`/`y` already made `buildWaferMap` throw, but a bin of `"1"`
119
+ is not pass bin `1`, so those dies counted as fails and yield read 0 %, and a test value of
120
+ `"0.5"` was not plotted or analysed as a number — with nothing to say so. `buildWaferMap` now
121
+ raises an `input-values-not-numbers` warning (severity `error`) counting text bins, text test
122
+ values and non-boolean verdicts, with an example. The values are not converted: convert them
123
+ and rebuild.
124
+ - **Changing the bin palette inside an expanded gallery card did not recolour the map.** A card
125
+ keeps the gallery-wide bin colours so every card agrees, and those took precedence over the
126
+ palette chosen in the card's own menu, so the menu appeared to do nothing — most visibly for soft
127
+ bins. A palette or "Use colours from bin definitions" change made in the card now resolves the
128
+ card's colours from that choice; a change made in the gallery still recolours every card
129
+ together.
130
+ - **A card reattached to the gallery kept the view it had while expanded.** Reattaching stored the
131
+ expanded window's options — plot mode, palettes, overlays — as that card's per-card overrides,
132
+ which win every time the card is built. A card switched to soft bins while expanded stayed a
133
+ soft-bin map under a gallery bar and legend strip describing hard bins, and later gallery palette
134
+ changes left it in colours matching no other card. A reattached card now takes the gallery's
135
+ shared options again, like every other card. This also stops reattaching from discarding
136
+ per-card `viewOptions` the host set on the item.
137
+ - **A lot report built for wafers without a precomputed summary analysed empty wafers.** The
138
+ report ran its lot analysis on `{ label, wafer, dies, passBins }` pieces, which the analysis
139
+ does not recognise as built maps, so it treated each as a fresh input with no results. The
140
+ report then had no findings, `N/A` per-wafer yields, and a title of "Summary — N wafers"
141
+ instead of "Lot Summary — <lot>". It now analyses the built maps (`renderLotReportHtml`, and
142
+ the gallery's and lot panel's report buttons, which use it).
143
+ - **A gallery with a fixed column count left most of the row empty.** Picking 2 columns drew two
144
+ 480px cards and left the rest of the row empty, because a fixed column count was still capped
145
+ by die density (since 0.21.1). A fixed column count now divides the full width; Auto keeps the
146
+ cap.
147
+ - **`GalleryOptions.columns` was ignored at mount**, so the grid always drew a single column.
148
+ Invalid counts (`0`, negative, `NaN`) now fall back to automatic layout, and fractions round.
149
+
150
+ ### Added
151
+
152
+ Supported replacements for exports deprecated in 0.30.0 that had no other path. Each deprecated
153
+ export's notice names its replacement; the old names are still removed in 0.31.0.
154
+
155
+ - **Capability, pass rates by recorded verdict, and region yield in the analysis output**
156
+ (API §7.4.1), replacing `buildCapabilityData`, `buildTestPassRateData`/`hasJudgeableTests`
157
+ and `buildRegionYieldData` with the ring/quadrant builders:
158
+ - `stats.capability` — Cp/Cpk/Pp/Ppk per parametric test, computed when per-test statistics
159
+ are (`computePerTestStats`). On a lot summary each wafer is a subgroup, so `stdWithin` is the
160
+ pooled within-wafer stddev and Cp and Pp differ. The chart's normalised five-number fields
161
+ are not included; they are not the test's real minimum and quartiles.
162
+ - `stats.testFlagYield` — per-test pass rate by the tester's recorded verdict — and
163
+ `stats.specVerdictDisagreementDies`, the dies where that verdict and the spec-limit judgement
164
+ disagree. They complement the existing `testSpecYield` and `functionalYield`.
165
+ - `stats.regionYield` — `{ ring, quadrant }` yield with each region's die and pass counts, each
166
+ die judged by its own wafer's pass bins.
167
+ - On `LotStatsSummary.stats`: `capability`, `regionYield`, `testSpecYield`, `functionalYield`,
168
+ `testFlagYield` and `specVerdictDisagreementDies`, all pooled exactly from the wafer summaries
169
+ — counts summed, and capability from each wafer's moments — so a lot analysis given
170
+ `perWaferSummaries` stays cheap. A pooled figure is absent unless every wafer reported it. Lot ring yield is also absent when wafers
171
+ were built with different ring counts.
172
+ - `analyzeWaferLot` now builds a raw `WaferMapInput` once and passes the built map to each
173
+ wafer's analysis.
174
+ - **`renderWaferReportHtml(result, summary?)` and `renderLotReportHtml(results)`**, replacing
175
+ `renderSummaryReportHtml` and `renderLotSummaryReportHtml`. They take built maps, so pass bins
176
+ and ring count come from the map rather than defaulting to `[1]` and `4`. They need no DOM.
177
+ The Summary panel's report buttons use them.
178
+ - **`binColorsForMaps(results, options?)` and `getBinColors()` on `WaferMapController` and
179
+ `GalleryController`** — the colours the maps draw, for a host's own table, export or chart —
180
+ replacing `resolveBinColors` and `getBinColorScheme`. `binColorsForMaps` judges each map by its
181
+ own pass bins; the gallery's own colouring now goes through the same function.
182
+ - **`buildWaferMap({ layout: true, waferConfig, dieConfig })`** — a die layout with no test data:
183
+ every site fully on the wafer (the gross die count), die `(0, 0)` at the centre, counted in the
184
+ configured axis directions. It replaces `createWafer` + `generateDies` + `clipDiesToWafer`, and
185
+ renders like any other result.
186
+ - **`stats.spatialPattern` on `analyzeWaferMap`'s result** — the spatial pattern classifier's label,
187
+ confidence and geometry features (failure density overall and at the edge, the failing
188
+ cluster's radial position, eccentricity, linearity, …), replacing a direct `classifyPattern`
189
+ call. It is present for every wafer the classifier can measure, including those labelled
190
+ `'random'` or `'none'`, which raise no finding, so the features can feed a model of your own with
191
+ negative examples as well as patterned ones.
192
+ - **`ICONS.collapse` and `ICONS.uncollapse`**, the floating-window header's collapse and
193
+ show-contents icons.
194
+ - **`WaferMapController.closeSummaryPanel()` and `GalleryController.setColumns()` are restored.**
195
+ Both were removed in 0.30.0 without a deprecation period. A host needs the first to get the
196
+ map's full width back, for example when loading a new file, and the second to apply its own
197
+ column control or a saved preference without rebuilding the gallery. Closing the panel no
198
+ longer resets the Summary button's notable-findings colour, as the 0.29 version did.
199
+
200
+ ## [0.30.0] — 2026-09-15
201
+
202
+ ### Breaking
203
+
204
+ - **Bin colour is keyed by bin number again, not by die count.** Since 0.28.0,
205
+ `resolveBinColors` ranked bins by die count and handed out palette colours in that order, so
206
+ a colour meant "the biggest fail bin in this view" rather than any particular bin: two lots
207
+ of one program drew hard bin 7 red in one and brown in the other, and filtering a gallery
208
+ could recolour bins. Industry wafer-map tools key colour on the bin number so engineers can
209
+ learn a program's colours and compare screenshots; wmap now does the same. A pass bin takes
210
+ `pass[(bin − 1) mod n]` and a fail bin `fail[(bin − 2) mod n]`, so bin 1 is the first green and
211
+ bin 2 is red in the default palette. Still pass/fail-aware (a failing bin 1 is never green),
212
+ still honours `BinDef.color`. The slot comes from the number alone, so changing `passBins`
213
+ recolours only bins whose verdict changed.
214
+ - **Soft bins read the palette shifted by half its length**, so hard bin *n* and soft bin *n*
215
+ are different colours by default.
216
+ - **`shared` now reports fixed pairs:** bins whose numbers are a palette-length apart (fail
217
+ bins 2 and 21 in `'default'`, 2 and 16 in `'accessible'`) when both are present, rather than
218
+ "more bins than colours".
219
+ - Same function, same types, different colours for the same input, which is why this is
220
+ breaking. Any host screenshot or test that pinned a rank-derived colour will change.
221
+ - **The `passBins` options on `analyzeWaferMap`, `analyzeWaferLot`, `renderWaferMap` and
222
+ `renderWaferGallery` are removed.** Pass bins are set once, on `buildWaferMap`'s input, and
223
+ carried on the result as `WaferMapResult.passBins` (new), which all four read — per wafer in a
224
+ gallery or lot. A second place to set them is how every surface came to judge pass/fail by `[1]`
225
+ (see Fixed), and an analysis override produced a summary whose findings and yield figure used
226
+ different pass bins. A map not built by `buildWaferMap` states them in that same `passBins`
227
+ field. Passing a removed option is a type error; at runtime `analyzeWaferMap` and
228
+ `analyzeWaferLot` ignore it and say so with an `analysis-option-corrected` warning, as they do
229
+ for a stale `ringCount`. To analyse or render with different pass bins, rebuild.
230
+ - **Options and methods removed as unused, duplicated or dead.** Found by measuring what tsmap,
231
+ the examples and the library itself actually use; each was either never set by anything, a
232
+ second way to set something that already had one, or accepted and ignored.
233
+ - `RenderOptions`: `minZoom`/`maxZoom` (fixed at 0.4× and 20× of the fitted view),
234
+ `renderTooltip`, `maxSize`, `toolbarControls` (its `'view-only'` value was never used),
235
+ `showPlotModeSelector`, and the top-level `legendPosition` and `fallbackFormat` — set both
236
+ through `viewOptions`.
237
+ - `GalleryOptions`: `cardPadding`, `maxSize` (the density-derived card size cap always
238
+ applies), `showPlotModeSelector`, `legendPosition` and `fallbackFormat` (through `viewOptions`).
239
+ - `WaferMapController`: `setDies` (it replaced dies but not the wafer geometry, so switching
240
+ wafers with it kept the previous wafer's outline — use `setResult`), `setFallbackFormat` (use
241
+ `setOptions({ fallbackFormat })`), `setHelpButtonVisible`, `setTooltipParent`.
242
+ `GalleryController`: `setFallbackFormat`, `setColumns`.
243
+ - `ViewOptions.dieGap` (fixed at 1 mm), `ToCanvasOptions.topClearance` (always passed 0),
244
+ `DieListDisplayOptions.csvFilename`.
245
+ - `AnalyzeWaferMapOptions.includePartial` / `includeEdgeExcluded`: analysing dies that yield
246
+ excludes could only make the findings describe a different population from the yield figure
247
+ beside them. `isYieldEligibleDie`'s own options are unchanged.
248
+ - `fallbackFormat` is now a view preference (`WaferPreferences.fallbackFormat`), reported
249
+ through `onViewOptionsChange` with the others.
250
+ - **Gallery-to-card plumbing is off the public API.** `WaferDisplayState` loses `binColors`,
251
+ `metadataValueOrder` and `lotSize`; `RenderOptions` loses `chromeInset` and `onExpand`;
252
+ `WaferMapController` loses `setSummaryVisible` and `setViewControlsVisible`. The gallery set
253
+ all of them on its own cards, and on a host's map they read as settings a host must supply to
254
+ get a valid map — a lone map resolves its bin colours, metadata order and lot size itself.
255
+ `renderWaferGallery`'s `getOptions()` and `onViewOptionsChange` no longer carry the three
256
+ fields either (`binColors` held `Map`s, which a host persisting its options could not
257
+ serialise). `ViewOptions.binColors`, `.metadataValueOrder` and `.lotSize` for `buildView` are
258
+ unchanged.
259
+ - **Ring count is set once, on `buildWaferMap`.** `WaferMapInput.ringCount` (default 4) is
260
+ carried as `WaferMapResult.ringCount`, and the ring overlay, ring findings, Summary panel,
261
+ report and Insights all read it. `AnalyzeWaferMapOptions.ringCount` and the view preference
262
+ `ringCount` are removed: they were two places that had to match, and when they did not, the
263
+ ring boundaries on the map described different rings from the ring findings beside them.
264
+ Validation moved with it — a bad value is corrected and reported in `result.warnings` as
265
+ `analysis-option-corrected`, still with no upper bound. A gallery whose wafers were built with
266
+ different ring counts raises the new `ring-count-mixed` warning; each card and each wafer's
267
+ findings use their own. `ViewOptions.ringCount` (for `buildView`) and `classifyDie`'s option
268
+ are unchanged.
269
+ - **`BinColors` has a new required field, `pass: { hard: Set<number>; soft: Set<number> }`** —
270
+ the bins that pass, per type, as `resolveBinColors` judged them. A host that builds a
271
+ `BinColors` by hand must add it; an object without it passed as `ViewOptions.binColors` is
272
+ now ignored and resolved afresh rather than trusted. `binPassSets(dies, passBins)` gives the
273
+ same sets without colours.
274
+ - **`WaferMapResult.inference.warnings` is removed.** It has been a deprecated string mirror since
275
+ 0.13.5, when `WaferMapResult.warnings` replaced it with the same messages plus a stable `code` and
276
+ a severity. Read `result.warnings`. Internally each geometry advisory now records its code where it
277
+ is detected, rather than having it recovered by matching phrases in the message.
278
+ - **`View.colorBySpec` is removed.** Its input option went in 0.21.0; the output stayed, only ever
279
+ equal to `passFailDisplay === 'spec'`. Read `view.passFailDisplay`.
280
+ - **`ToCanvasOptions.activeBin`, `.hoverBin` and `.minRightReserve` are removed.** They carry the
281
+ interactive map's own legend highlight, pointer and mode-switch layout state, which a direct
282
+ `toCanvas` caller has none of — the same reason `RenderOptions` already dropped them.
283
+ - **`AnalyzeWaferMapOptions` loses its per-analysis switches:** `enableYieldAnalysis`, `enableHardBinAnalysis`, `enableSoftBinAnalysis`, `enableReticlePositionAnalysis`, `enableTestSiteAnalysis`, `enableClusterAnalysis`, `enableAngularAnalysis` and `enablePatternClassification`.
284
+ Each was cheap and on by default; every analysis now runs, and a caller wanting fewer findings
285
+ filters them (`filterFindings`). Passing one from untyped JavaScript is reported as
286
+ `analysis-option-corrected`. The two that cost real time stay: `enableTestValueAnalysis`, and
287
+ `computePerTestStats`, which triples analysis time on a 7,843-die, 50-test wafer (94 ms → 298 ms)
288
+ and so cannot simply always run.
289
+ - **`WaferMapController.setExpandVisible`, `setIdentityVisible`, `closeSummaryPanel` and
290
+ `getActiveLegend` are removed.** The first two duplicated the `showExpandButton` and `showIdentity`
291
+ options, and nothing used the other two. The gallery keeps expand-button control internally.
292
+
293
+ ### Fixed
294
+
295
+ - **A wafer with no die positions covered the Insights view.** In `renderWaferMap`, a
296
+ coordinate-less wafer's "No die position data" summary stayed on top of the chart suite once
297
+ Insights was opened, hiding most of it. A mixed wafer's "+N dies without position data" footer
298
+ did the same. Their z-index beat the Insights layer's. The map view is now hidden outright while
299
+ Insights is open, so nothing inside it can show through, and its controls leave the tab order.
300
+
301
+ - **Input names removed in earlier releases vanished without a trace.** A plain-JavaScript caller
302
+ still passing `data` instead of `results` got an empty map, and `values` instead of `testValues`
303
+ a map with no test data — no error, no warning, nothing that looked wrong. `buildWaferMap` now
304
+ reports every removed name it finds (`data`, `die`, `stack`, `values`, `TestDef.index`,
305
+ `dieConfig.origin`, `waferConfig.flat`, `reticleConfig.anchor`, `lotStack.aggr`) as a new
306
+ `input-field-removed` warning naming each replacement, in `result.warnings` and on the console.
307
+ It still does not honour them: rename and rebuild.
308
+ - **Pass bins given to `buildWaferMap` were used for `result.yield` and nothing else.** The result
309
+ had nowhere to keep them, so every later surface fell back to `[1]` unless the caller repeated
310
+ them: `analyzeWaferMap`'s findings and yield statistics, bin colours and legend order, the
311
+ failing-die hatch, the Summary panel and report, region yield, the Insights yield charts, the
312
+ gallery strip's Yield, and the map's own internal view. A program whose bins 1 and 2 both pass
313
+ therefore showed bin 2 as a failure everywhere except the one yield figure — and the docs
314
+ already claimed `analyzeWaferMap` inferred them from the result. tsmap hit this on every file
315
+ whose pass bins are not just bin 1: it gives each wafer's pass bins to `buildWaferMap` and to
316
+ nothing else. `[1]` is now a default only at `buildWaferMap`'s input; `WaferMapResult.passBins`
317
+ carries the value everywhere after.
318
+ - **A gallery judges each wafer by its own pass bins**, so a lot mixing test programs is
319
+ correct per wafer in cards, strip yield, lot panel, report, region yield and Insights. Hard
320
+ bins that pass on one wafer and fail on another get one colour and one legend row, so the
321
+ gallery raises `pass-bins-mixed` naming them. Yield labels say "per wafer: bin 1 · bins 1, 2"
322
+ when wafers disagree, rather than naming one wafer's set.
323
+ - The report's lot region-yield table and the gallery strip's yield were separate copies of
324
+ the per-die pass rule; both now use the shared ones (`buildRegionYieldData`, `diePassStatus`).
325
+ - **A map's bin legend drew its title over the first row when there were many bins.** Rows
326
+ were fitted to the full canvas height and the "Soft Bin"/"Hard Bin" title placed above them
327
+ afterwards, clamped below the toolbar, so a legend tall enough to fill the height had its
328
+ title printed across row one. The title row (and a floating legend's padding and heading) is
329
+ now reserved before rows are fitted; overflow moves into "+ N more" one row sooner.
330
+ - **Bin legends list pass bins first, then failing bins by die count**, the order the Summary
331
+ panel, report and Insights pareto already used (`sortBinsForDisplay`). The per-map canvas
332
+ legend and the gallery's legend strip both sorted by bin number, so one lot was listed two
333
+ ways on one screen, and a program with dozens of bins buried its biggest failures.
334
+ - **Soft bins were ordered, and the gallery's soft-bin yield was totalled, as if hard pass bin
335
+ numbers applied to them.** The Summary panel, both report bin tables and the gallery strip
336
+ judged a soft bin "pass" by looking it up in `passBins`, which holds hard-bin numbers: soft bin
337
+ 1 sorted as a pass and soft bin 100 as a fail whatever their dies did, and the gallery strip's
338
+ Yield read about 0% in soft-bin mode for any program whose passing soft bins are numbered
339
+ differently. They now use the soft bin's own verdict (`BinColors.pass.soft`).
340
+ - **A gallery card had no title unless the host passed `label`, and every other surface named
341
+ that wafer differently.** Without `label` the card header was blank, so an engineer could not
342
+ tell which wafer a card showed. The findings list, the lot report, the yield list and the die
343
+ list's Wafer column called it `W3`, a position that reads like a wafer ID and need not match
344
+ the real one; a detached window called it "Wafer map"; and the Insights tab used the wafer ID.
345
+ The wafer ID was on every item. One internal rule, `waferDisplayLabel` (`core/waferLabel.ts`),
346
+ now names a wafer everywhere: the host's `label`, else `wafer.metadata.waferId`, else
347
+ "Wafer 3 (no ID)". Hosts that pass `label` see no change.
348
+ - **The gallery's Legend style menu offered per-card legend positions as if they applied to
349
+ a legend nobody could see.** Per-card legends are off by default in `renderWaferGallery`
350
+ (the lot legend strip stands in for them), yet the menu listed the six positions first,
351
+ live, with **Default (right)** ticked, and the **Legend on each map** toggle last. It read
352
+ as though the positions moved the lot strip. The toggle now comes first, and the positions
353
+ follow under **Position on each map**, greyed and unticked, with the reason as a tooltip,
354
+ until per-card legends are on. The single-map menu, which has no toggle, is unchanged.
355
+ The rule is in `makeLegendStyleBtn` (`toolbar.ts`).
356
+
357
+ ### Changed
358
+
359
+ - **Segments of the gallery legend's population bar are now separated by a 1px gap.**
360
+ Segments are ordered by die count, so any two palette colours can sit side by side, and at
361
+ 8px tall the darker ones (black, indigo, dark teal, brown) and the pass greens ran together,
362
+ so the bar read as fewer, wider bins than it held. The gap is transparent, so
363
+ it shows the strip's own background in either theme, and it adds to each segment's 2px
364
+ minimum width rather than eating into it.
365
+
366
+ ### Deprecated
367
+
368
+ - **`valueToViridis`, `valueToGreyscale` and `getValueColorScheme`**, to be removed in
369
+ 0.31.0. Each still works, logs one console notice on first use, and is struck
370
+ through in editors. Use `resolveValueColorFn(name, reversed)` instead:
371
+ `resolveValueColorFn('default')` and `resolveValueColorFn('greyscale')` return exactly the
372
+ colours the first two did. None of the three applies `reverseValueScheme`, so a legend, chart
373
+ or export built on them could show a reading in a different colour from the map.
374
+ `listValueColorSchemes()` gives scheme names and labels.
375
+ - **The chart-data builders**, to be removed in 0.31.0: `buildYieldData`,
376
+ `buildYieldDataCombined`, `buildBinParetoData`, `buildBinClusterData`, `buildCapabilityData`,
377
+ `buildTestBoxplotData`, `buildTestTrendData`, `trendCentre`, `buildTestPassRateData`,
378
+ `hasJudgeableTests`, `buildTestHistogramData`, `buildTestHistogramSeries`,
379
+ `buildCorrelationMatrix`, `filterCorrelationMatrix`, `buildScatterData` and
380
+ `buildScatterDataGrouped`. They were made public so a host could draw the Insights charts
381
+ itself, before the Insights tab (`insights: { enabled: true }`) drew them; neither tsmap nor
382
+ any example calls them. Each still works and logs one console notice on first use. There is
383
+ no replacement: if you depend on one, say so at https://github.com/wafertools/wafermap/issues. Their types stay until the
384
+ functions go. `buildFacetTable`, `facetValueOf` and `mergeTestDefs` are not
385
+ deprecated.
386
+ - **The low-level drawing pipeline**, to be removed in 0.31.0: `buildView`, `toCanvas`, `createWafer`,
387
+ `generateDies`, `clipDiesToWafer`, `applyOrientation`, `transformDies`, `applyProbeSequence`,
388
+ `generateReticleGrid`, `mapDataToDies`, `isInsideWafer`, `getReticleCell`, `resolveGridPitch`,
389
+ `classifyDie`, `getRingLabel`, `aggregateValues`, `aggregateBinCounts`, `getUniqueBins`,
390
+ `buildHoverText`, `buildMapTitle` and the eight `affine*` helpers. It let a host draw a map without
391
+ `renderWaferMap`; nothing known does — tsmap draws every map through the renderers — and only the
392
+ pipeline example used it, which is removed. The types only the pipeline needs (`ViewOptions`,
393
+ `ToCanvasOptions` and the rest) go with it.
394
+ - **Helpers exported by accident**, to be removed in 0.31.0: the region builders (`buildRingRegions`,
395
+ `buildQuadrantRegions`, `buildSectorRegions`, `buildReticlePositionRegions`, `buildTestSiteRegions`,
396
+ `buildRegionYieldData`, `areQuadrantsAdjacent`, `parseRegionKey`, `sectorCompassNames`),
397
+ `classifyPattern`, `visibleFindings`, `computeFunctionalYield`, `resolveMetadataColumns`,
398
+ `discoverDieMetadataKeys`, the report builders (`renderSummaryReportHtml`,
399
+ `renderLotSummaryReportHtml`, `renderFindingsReportHtml`, `openHtmlReport`, `openReportModal`),
400
+ `resolveBinColors`, `getBinColorScheme`, `contrastTextColor`, `getDieTestValue`, `dieHasTestData`,
401
+ `isParametricTest`, `isPositionedDie`, `metadataDisplayValue`, `metadataCategoricalValue`,
402
+ `buildDieListSection`, `DEFAULT_FACET_CURATION` and `STANDARD_WAFER_DIAMETERS_MM`. The library
403
+ applies each itself. Staying, because each carries a rule or a hook a host needs: `getDieKey`,
404
+ `getTestPassStatus`, `diePassStatus`, `isYieldEligibleDie`, `hasPosition`, `filterFindings`,
405
+ `resolveValueColorFn`, `FACET_NONE_VALUE`, `setReportOpener` and `setDetachWindowOpener`. The two
406
+ constants cannot log a notice; the functions do.
407
+ - **The 0.31.0 removal is enforced**, not just announced: a test fails once the changelog or
408
+ `package.json` reaches 0.31.0 while any of these exports is still there.
409
+
410
+ ### Docs
411
+
412
+ - **New example, "Bin colours across a full program"** (`docs/examples/bin-colours.html`).
413
+ A four-wafer gallery over a new generated dataset, `showcase-bin-rich`: 15 hard bins with two
414
+ pass grades and gaps in the numbering, and 32 soft bins numbered under their hard bin. Each
415
+ failure mode has its own spatial pattern. Every other bundled dataset has 2–6 bins, which is
416
+ too few to show how bin colour behaves across a real program. The soft bins deliberately
417
+ outnumber the palette, so the example also shows the `bin-colors-shared` warning. The data
418
+ comes from `scripts/gen-showcase-csvs.mjs` like the other showcase files.
419
+
420
+ ### Internal
421
+
422
+ - **CI runs once per push to `main`, not twice.** `ci.yml` had its own `push` trigger while
423
+ `deploy.yml`, which also runs on every push, calls it as its gating job, so the same suite
424
+ ran twice in parallel on each commit. `ci.yml` now triggers only on pull requests (and as
425
+ a reusable workflow). `deploy.yml` is therefore the only thing testing pushes to `main`,
426
+ and says so. The README badge now points at `deploy.yml`.
427
+ - **The perf suite's ratio tests use the fastest of N runs instead of the median**
428
+ (`fastest()` in `tests/perf.test.mjs`). The two paths in a ratio are timed one after the
429
+ other, so background load inflated them unequally. The value-vs-hardBin check failed an
430
+ `npm publish` at 2.86× on a loaded machine where both paths had slowed about 7×. Absolute
431
+ budget tests keep the median, where a minimum would weaken the guard. The value-mode limit
432
+ stays at 2.5×: measured, the steadier statistic does not buy back enough headroom under
433
+ CPU saturation to tighten it.
434
+
25
435
  ## [0.29.0] — 2026-09-12
26
436
 
27
437
  ### 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-~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 ~46 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,13 +72,13 @@ 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.
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.
@@ -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()}}