@wafertools/wafermap 0.30.4 → 0.32.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 (103) hide show
  1. package/AGENTS.md +21 -31
  2. package/CHANGELOG.md +265 -1
  3. package/README.md +3 -3
  4. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +89 -6
  6. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/scatter.d.ts +11 -1
  9. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  11. package/dist/packages/canvas-adapter/dieList.d.ts +1 -1
  12. package/dist/packages/canvas-adapter/dieList.js +7 -7
  13. package/dist/packages/canvas-adapter/drilldown.js +1 -1
  14. package/dist/packages/canvas-adapter/exportName.d.ts +1 -12
  15. package/dist/packages/canvas-adapter/exportName.js +1 -1
  16. package/dist/packages/canvas-adapter/index.d.ts +2 -3
  17. package/dist/packages/canvas-adapter/index.js +1 -1
  18. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  19. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +4 -7
  20. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  21. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +32 -34
  22. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  23. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  24. package/dist/packages/canvas-adapter/toCanvas.d.ts +23 -2
  25. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  26. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  27. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  28. package/dist/packages/canvas-adapter/userGuideHtml.js +38 -24
  29. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  30. package/dist/packages/canvas-adapter/version.js +1 -1
  31. package/dist/packages/core/aggregates.d.ts +0 -6
  32. package/dist/packages/core/aggregates.js +1 -1
  33. package/dist/packages/core/dieTable.d.ts +137 -0
  34. package/dist/packages/core/dieTable.js +1 -0
  35. package/dist/packages/core/dies.d.ts +22 -2
  36. package/dist/packages/core/dies.js +1 -1
  37. package/dist/packages/core/index.d.ts +8 -10
  38. package/dist/packages/core/index.js +1 -1
  39. package/dist/packages/core/stdf.d.ts +9 -0
  40. package/dist/packages/core/stdf.js +1 -0
  41. package/dist/packages/core/transforms.d.ts +0 -23
  42. package/dist/packages/core/transforms.js +1 -1
  43. package/dist/packages/core/utils.d.ts +14 -0
  44. package/dist/packages/core/utils.js +1 -1
  45. package/dist/packages/renderer/buildView.d.ts +4 -12
  46. package/dist/packages/renderer/buildView.js +1 -1
  47. package/dist/packages/renderer/buildWaferMap.d.ts +42 -32
  48. package/dist/packages/renderer/buildWaferMap.js +1 -1
  49. package/dist/packages/renderer/colorMap.d.ts +0 -2
  50. package/dist/packages/renderer/colorMap.js +1 -1
  51. package/dist/packages/renderer/columnarInput.d.ts +87 -0
  52. package/dist/packages/renderer/columnarInput.js +1 -0
  53. package/dist/packages/renderer/deprecate.d.ts +1 -1
  54. package/dist/packages/renderer/deprecate.js +1 -1
  55. package/dist/packages/renderer/derivedTests/apply.d.ts +8 -6
  56. package/dist/packages/renderer/derivedTests/apply.js +1 -1
  57. package/dist/packages/renderer/index.d.ts +7 -5
  58. package/dist/packages/renderer/index.js +1 -1
  59. package/dist/packages/renderer/spec.d.ts +3 -7
  60. package/dist/packages/renderer/spec.js +1 -1
  61. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  62. package/dist/packages/stats/analyzeWaferMap.d.ts +134 -2
  63. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  64. package/dist/packages/stats/boxplot.js +1 -1
  65. package/dist/packages/stats/capability.d.ts +7 -7
  66. package/dist/packages/stats/capability.js +1 -1
  67. package/dist/packages/stats/clusterDetection.d.ts +2 -1
  68. package/dist/packages/stats/clusterDetection.js +1 -1
  69. package/dist/packages/stats/connectedComponents.js +1 -1
  70. package/dist/packages/stats/correlation.js +1 -1
  71. package/dist/packages/stats/facets.d.ts +1 -1
  72. package/dist/packages/stats/filterFindings.d.ts +5 -0
  73. package/dist/packages/stats/filterFindings.js +1 -1
  74. package/dist/packages/stats/histogram.js +1 -1
  75. package/dist/packages/stats/index.d.ts +6 -16
  76. package/dist/packages/stats/index.js +1 -1
  77. package/dist/packages/stats/math.d.ts +22 -6
  78. package/dist/packages/stats/math.js +1 -1
  79. package/dist/packages/stats/normalizeInput.js +1 -1
  80. package/dist/packages/stats/patternClassification.d.ts +5 -4
  81. package/dist/packages/stats/patternClassification.js +1 -1
  82. package/dist/packages/stats/regions.d.ts +9 -1
  83. package/dist/packages/stats/regions.js +1 -1
  84. package/dist/packages/stats/renderFindingsReport.d.ts +0 -4
  85. package/dist/packages/stats/renderFindingsReport.js +1 -21
  86. package/dist/packages/stats/renderSummaryReport.js +18 -18
  87. package/dist/packages/stats/scatter.js +1 -1
  88. package/dist/packages/stats/sweep.js +1 -1
  89. package/dist/packages/stats/testPassRate.js +1 -1
  90. package/dist/packages/stats/trend.js +1 -1
  91. package/dist/packages/stats/types.d.ts +5 -0
  92. package/dist/packages/worker/index.js +1 -1
  93. package/dist/packages/worker/wafermap.worker.d.ts +3 -0
  94. package/dist/packages/worker/wafermap.worker.js +1 -1
  95. package/package.json +2 -2
  96. package/dist/packages/canvas-adapter/deprecated.d.ts +0 -7
  97. package/dist/packages/canvas-adapter/deprecated.js +0 -1
  98. package/dist/packages/core/deprecated.d.ts +0 -60
  99. package/dist/packages/core/deprecated.js +0 -1
  100. package/dist/packages/renderer/deprecated.d.ts +0 -45
  101. package/dist/packages/renderer/deprecated.js +0 -1
  102. package/dist/packages/stats/deprecated.d.ts +0 -85
  103. package/dist/packages/stats/deprecated.js +0 -1
package/AGENTS.md CHANGED
@@ -19,7 +19,7 @@ loudly over guessing.
19
19
 
20
20
  ### Entry points
21
21
 
22
- - `@wafertools/wafermap` — `buildWaferMap()`, geometry, `registerBinColorScheme()` / `registerValueColorScheme()`. Pure, no DOM, server-safe.
22
+ - `@wafertools/wafermap` — `buildWaferMap()`, `registerBinColorScheme()` / `registerValueColorScheme()`. Pure, no DOM, server-safe.
23
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.
@@ -27,22 +27,13 @@ loudly over guessing.
27
27
  Default path: `buildWaferMap()` once when data loads, then `renderWaferMap()` for a
28
28
  single wafer or `renderWaferGallery()` for several.
29
29
 
30
- **The types still export a large deprecated surface that is removed in 0.31.0 — do not
31
- reach into it just because autocomplete offers it.** Four groups, all replaced by the
32
- default path above:
30
+ **There is no low-level drawing pipeline, chart-data builder or region builder to reach
31
+ for.** The library draws through the renderers, and every figure it shows (yield, bin
32
+ counts, region yield, per-test statistics, capability) comes back from
33
+ `analyzeWaferMap()` / `analyzeWaferLot()`.
33
34
 
34
- - the low-level drawing pipeline — `buildView()`, `toCanvas()`, `createWafer()`,
35
- `generateDies()`, and the geometry and transform helpers around them;
36
- - the chart-data builders — `buildYieldData()`, `buildCorrelationMatrix()`,
37
- `buildTestBoxplotData()` and the rest: these were the internals of the Insights tab,
38
- which the renderers now mount for you (see below);
39
- - the region builders — `buildRingRegions()`, `buildQuadrantRegions()` and friends:
40
- region yield comes back from `analyzeWaferMap()`;
41
- - per-die and per-colour helpers — `getDieTestValue()`, `buildHoverText()`,
42
- `resolveBinColors()`, `getValueColorScheme()`, `valueToViridis()`.
43
-
44
- If the only way to do something is through one of these, that is a library gap worth
45
- reporting, not a pattern to build on.
35
+ If the only way to do something is to rebuild one of those pieces, that is a library gap
36
+ worth reporting, not a pattern to build on.
46
37
 
47
38
  ### Traps that produce silently wrong maps
48
39
 
@@ -54,12 +45,11 @@ reporting, not a pattern to build on.
54
45
  pre-multiply. The geometry inputs are `waferConfig` (type `WaferConfig`) and
55
46
  `dieConfig` (type `DieConfig`) — both optional, both inferred when omitted.
56
47
  - **Bins and test values must be numbers, and verdicts booleans.** Every parser —
57
- CSV, JSON, a spreadsheet export — hands you `"1"`, and `"1"` is not pass bin 1: those
58
- dies count as fails and the yield is wrong, while a test value left as text is not
59
- plotted or analysed correctly. Convert with `Number()` at parse time. `buildWaferMap`
60
- samples the input and reports `input-values-not-numbers` (severity `'error'`) rather
61
- than coercing behind your back, so a build that "works" can still be wrong — read the
62
- warnings.
48
+ CSV, JSON, a spreadsheet export — hands you `"1"`, and `"1"` is not pass bin 1.
49
+ `buildWaferMap` never converts them: each is treated as missing and reported as
50
+ `input-values-not-numbers` (severity `'error'`), so those dies have no bin or no value
51
+ and a build that "works" can be mostly empty — read the warnings. Convert with
52
+ `Number()` at parse time.
63
53
  - **`passBins` and `ringCount` are set once, on `buildWaferMap`, and travel on the
64
54
  result.** Neither is an option on `analyzeWaferMap`, `analyzeWaferLot`,
65
55
  `renderWaferMap` or `renderWaferGallery` — passing one there is a type error in
@@ -124,9 +114,8 @@ reporting, not a pattern to build on.
124
114
  `insights: { enabled: true }` and it mounts the chart suite (yield, bin pareto,
125
115
  boxplot, histogram, correlation, scatter, capability, and one card per
126
116
  `insights.sweeps` entry). `renderWaferGallery` takes the same option across a whole
127
- lot. Supply or replace the analysis later with `setStatsSummary()`. Hand-building
128
- those charts is what the deprecated chart-data builders were for, and they go in
129
- 0.31.0. Charting a selection or one wafer (right-click → histogram, capability,
117
+ lot. Supply or replace the analysis later with `setStatsSummary()`. There is no
118
+ chart-data API to hand-build them from. Charting a selection or one wafer (right-click → histogram, capability,
130
119
  sweeps) is built in too, with no wiring.
131
120
  - **A value computed from other tests is a derived test, not a host-side column.** Pass
132
121
  `derivedTests` (a `TestDef` plus an `expression`, e.g. `'abs(t[1020] - t[1010])'`) to
@@ -147,9 +136,9 @@ reporting, not a pattern to build on.
147
136
  but `'traffic'` and `'jet'` reads low = dark, high = light; if you draw your own
148
137
  colorbar or swatch, resolve the colour through `resolveValueColorFn(name, reversed)`
149
138
  so it cannot disagree with the dies.
150
- - `result.view` is internal. Use the promoted fields: `result.plotMode`,
151
- `result.metadata`, `result.isLotStack`, `result.hbinDefs`, `result.sbinDefs`,
152
- `result.testDefs`.
139
+ - What a map was built with is on the result: `result.plotMode`, `result.metadata`,
140
+ `result.isLotStack`, `result.hbinDefs`, `result.sbinDefs`, `result.testDefs`. The renderers
141
+ read them; pass nothing again.
153
142
  - Die keys come from `getDieKey(die)`. A hand-rolled `` `${x},${y}` `` breaks
154
143
  click-to-highlight silently, because findings carry `dieKeys` in that exact format.
155
144
  - `stats.warnings` is `WaferWarning[]` (it was `string[]` before 0.22.0). Read
@@ -190,9 +179,10 @@ it is handed, because it has no way to know which tests anyone will look at.
190
179
  `computePerTestStats` is modest; `enableTestValueAnalysis` is the expensive one
191
180
  (roughly 10× the base analysis on a large wafer) and exists to find spatial
192
181
  patterns automatically — do not enable it by default just because it sounds good.
193
- - **A Web Worker buys responsiveness, not speed.** `createWafermapWorker` copies data
194
- across `postMessage`, so total time goes *up*. Use it when a build would otherwise
195
- visibly freeze the page, not for small datasets.
182
+ - **A Web Worker buys responsiveness, not speed.** `createWafermapWorker` moves a
183
+ result's test values to the page without copying them, but still copies the dies and the
184
+ input, so total time goes *up*. Use it when a build would otherwise visibly freeze the
185
+ page, not for small datasets.
196
186
  - **Capability, pass rates, region yield and the spatial-pattern label come back from
197
187
  the analysis** — `stats.capability` (with `computePerTestStats`), `stats.testSpecYield`,
198
188
  `stats.testFlagYield`, `stats.functionalYield`, `stats.regionYield` and
package/CHANGELOG.md CHANGED
@@ -22,7 +22,271 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
25
- ## [Unreleased]
25
+ ## [0.32.0] — 2026-09-27
26
+
27
+ ### Breaking
28
+
29
+ - **Input values of the wrong type are treated as missing.** A bin, site number or test value
30
+ that is not a number, or a pass/fail verdict that is not `true`/`false`, is left out of the
31
+ die — the same rule as a value outside the STDF V4 ranges — and still reported as
32
+ `input-values-not-numbers`. A die whose only bin was text therefore has no verdict rather
33
+ than a fail, which can change `yield` for such input. The input objects are not modified.
34
+ - **Test values on dies from `buildWaferMap` are read-only snapshots.** A map holds test values and
35
+ verdicts as one column per test. `die.testValues` and `die.testPass` on a built die build a frozen
36
+ object from those columns on each read, keeping nothing on the die, so `die.testValues !==
37
+ die.testValues`. Assigning to either field throws a `TypeError`; changing a key of the frozen
38
+ object throws in strict-mode code and is ignored otherwise. Pass the values to `buildWaferMap`,
39
+ or copy the die with your own object (`{ ...die, testValues: mine }`). A spread, `structuredClone` or `JSON.stringify` of a built die
40
+ gives plain objects with the same values. A die no longer shares the input record's
41
+ `testValues`/`testPass` objects, so changing the input after the build does not change the map.
42
+ Dies a host builds itself keep ordinary objects.
43
+ - **Pre-built `dies` get the same input checks as `results`.** Wrong-type and out-of-range
44
+ values are treated as missing, and a die whose coordinates STDF cannot store is kept as an
45
+ unpositioned die.
46
+ - **`WaferMapResult.view` is removed.** It was marked `@internal`: the renderers build their own
47
+ draw list whenever they draw, so a result no longer carries one. Read the result's own fields
48
+ (`plotMode`, `metadata`, `isLotStack`, `hbinDefs`, `sbinDefs`, `testDefs`). This makes each
49
+ result smaller (about 86 bytes less per die) and roughly halves the copy the Web Worker makes of
50
+ a result. The internal `dataAxisFlip` field takes its place.
51
+
52
+ ### Changed
53
+
54
+ - **The legend filters to several bins or metadata values at once.** Ctrl/Cmd+click on a legend
55
+ entry — the map's legend or the gallery's strip, where Ctrl/Cmd+Enter/Space works from the
56
+ keyboard — adds or removes a value; a plain click shows only that value, or clears the
57
+ filter when it is the only one shown. `highlightBin` accepts `number | number[]` and
58
+ `highlightMetadataValue` accepts `string | string[]`.
59
+ - **A finding and the legend filter agree.** Clicking a finding about a bin filters the legend
60
+ to that bin, and one about yield or a test clears the filter — in a single map and in the
61
+ gallery alike. Changing the legend filter releases the finding, as changing the selection
62
+ does, including a selection made on a gallery card while a lot finding is active.
63
+ - **Lot regional findings are tested on all wafers' data together.** A yield, bin,
64
+ functional pass-rate, limit-fail or test-value difference in a region is combined across
65
+ every wafer (Stouffer's Z over each wafer's own test, weighted by die count) and reported
66
+ with the wafer analysis's gates, redundancy collapse and opposite-region re-test, instead of
67
+ by counting wafers whose own analysis reported it — a pattern present on every wafer but
68
+ too faint on some to pass alone is the lot's pattern. The sentence gives the lot's figure
69
+ and "higher/lower on N/M wafers, all wafers' data combined", N counting the wafers whose
70
+ region differs in that direction; `stats.method` is `'stouffer-z'`. Clusters, edge arcs and
71
+ spatial-pattern labels are still counted by the wafers that report them. Lot findings keep
72
+ absorbed restatements in the list, marked by `absorbedIds`, as wafer findings do.
73
+ - **Selected dies are shown by fading the rest of the map.** Every unselected die is faded
74
+ towards the map background and the selection is outlined, so selected dies keep their full
75
+ colour and it is clear which dies are selected whatever the selection's shape — a block, a
76
+ ring, an edge arc. A finding highlighted from the Summary panel, a gallery lot finding and
77
+ `setSelection` are drawn the same way. The fade and outline sit under the axes and legend.
78
+ - **A map opens in select mode.** A drag draws a selection box; hold Space and drag, use the
79
+ arrow keys, or choose Pan in the toolbar to pan.
80
+ - **Clicking the only selected die again clears the selection.** Clicking a die inside a
81
+ larger selection still selects just that die.
82
+ - **Changing the selection on the map releases an active finding.** A click, box select,
83
+ right-click, Esc or `clearSelection` clears the Summary panel's active finding and its bin
84
+ highlight, so the panel never shows a finding the map no longer highlights.
85
+
86
+ ### Fixed
87
+
88
+ - **Regional findings report patterns confined to one region.** A bin, limit-fail or
89
+ functional-fail rate that is zero in the rest of the wafer and raised in a region (a bin
90
+ found only at the edge) counts as the largest relative change for the effect gate and for
91
+ severity, for every region family: rings, quadrants, sectors, reticle positions and test
92
+ sites. On a lot whose wafers carry bin 2 only at the edge, the lot finding reads "seen on
93
+ 8/8 wafers".
94
+ - **Yield and functional pass-rate findings are judged by their failure rate.** The relative
95
+ effect of a pass rate is measured on its failures: yield 98% → 94% is failures 2% → 6%, and
96
+ reaches the same verdict as the bin rate of the same dies.
97
+ - **A region is not reported as deviating only because another region deviates more.** A
98
+ finding opposite in direction to a stronger finding for the same variable and region family
99
+ must still hold when compared with the rest of the wafer without that region, so an edge
100
+ rich in a bin, a limit fail or high test values no longer makes the inner rings read as low
101
+ in it (or high in yield).
102
+ - **The soft bin that restates yield is absorbed into the yield finding**, as the hard pass bin
103
+ is: the one soft bin every passing die carries and no failing die does.
104
+ - **An edge ring made of scattered fails is classified as an edge ring.** The spatial-pattern
105
+ classifier recognises an edge ring from the failing dies' positions — most fails at the rim
106
+ and spread round at least 60% of it — when no connected group of fails is large enough to
107
+ judge shape by, and before the scratch rule, so a short run along the rim is not a scratch.
108
+ Against WM-811K: detection 86.4%, edge-ring recall 75% (precision 92%), scratch recall 24%.
109
+ - **Lot spatial-pattern rows count related labels together and state the lot's figures.**
110
+ Edge-ring and edge-local wafers count as one edge pattern, and centre and donut as one,
111
+ naming each label's wafer count ("Spatial pattern: edge (edge-ring on 5, edge-local on 2) —
112
+ seen on 7/13 wafers"); the sentence gives the lot's count, not any one wafer's confidence or figures.
113
+ The gallery highlights each counted wafer's failing dies.
114
+ - Findings, their severities and "seen on N wafers" counts can change.
115
+
116
+ ### Performance
117
+
118
+ - **Insights no longer blocks while its test statistics are computed.** The Overview's Test Values
119
+ table is built in slices, showing "Computing test statistics…" until it is ready, and when the
120
+ gallery's summary panel is computing the same statistics at the time, the two share the one
121
+ calculation instead of each doing it.
122
+ - **Gallery cards are drawn only when on screen.** A card below the fold keeps its map up to date
123
+ and is drawn as it scrolls into view. Printing and the gallery PNG draw every card first, so
124
+ both still show the whole gallery. On a 25-wafer, 266k-die lot, with 4 cards on screen:
125
+ switching to value mode takes 0.4 s in Chrome (was 1.1 s) and 0.5 s in WebKit (was 2.0 s);
126
+ the gallery opens in 1.8 s and 2.0 s.
127
+ - **The gallery draws about twice as fast in WebKit** (the desktop app on Linux and macOS, and
128
+ Safari). Die fills and outlines are drawn as many small canvas paths rather than one per
129
+ colour, which WebKit rasterises far faster. On a 25-wafer, 266k-die lot the gallery opens in
130
+ 4.0 s (was 7.0 s) and a switch to value or stacked mode takes 1.8–2.3 s (was 3.8–4.4 s).
131
+ Chrome draws identical pixels; in WebKit only anti-aliased edge pixels differ.
132
+ - **The lot Summary report opens about five times faster on a large lot.** Its Test Values table
133
+ finds each test's minimum, mean and maximum in one pass instead of sorting every die's value.
134
+ On a 25-wafer, 266k-die lot the "Summary report" button takes 0.4 s in Chrome (was 6.0 s) and
135
+ 0.9 s from click to report in WebKit (was 4.7 s). The report is unchanged (compared as HTML).
136
+ Boxplot quartiles and the histogram's outlier range are found the same way as the Summary
137
+ panel's, by selection, with the same figures.
138
+ - **Test-value analysis (`enableTestValueAnalysis`) is two to three times faster on large lots.**
139
+ Spec-limit findings count each region's dies in one pass instead of re-reading every die for
140
+ every region, the test list is read column by column, and per-test statistics find their
141
+ quartiles by selection. Findings are unchanged (compared byte for byte on a 266k-die lot with
142
+ 5,289 findings). On that lot: 18.9 s → 7.2 s in Chrome, 25.6 s → 10.7 s in WebKit.
143
+ - **Analysis is about a third faster in Chrome on large lots.** Merging findings in adjacent
144
+ regions, pairing hard and soft bins that cover the same dies, and assigning dies to regions
145
+ now compare die positions as numbers rather than as a text key per die. Findings are
146
+ unchanged (compared byte for byte on a 266k-die lot). On that lot, load-time analysis takes
147
+ 2.5 s in Chrome (was 3.7 s).
148
+ - **Stacked modes and mode switches in the gallery are faster.** Stacked cards key die positions by
149
+ number rather than by a string per die, and a map no longer rebuilds a key for every die on
150
+ each redraw unless dies are selected. On a 25-wafer, 266k-die lot in Chrome: switching to
151
+ stacked bins 2.0 s → 1.6 s, to value mode 1.3 s → 1.1 s.
152
+ - **The lot summary panel's test statistics are computed about three times faster.** The
153
+ quartiles of each test's pooled values are found by selection instead of sorting every value
154
+ (the same order statistics, so the same figures). On a 25-wafer, 266k-die lot the gallery
155
+ with its panel opens in 3.1 s in Chrome (was 3.9 s) and about 3.5 s in WebKit (was 4.0 s).
156
+ - **Insights opens several times faster on a large lot** (25 wafers, 266k dies: the Overview
157
+ 2.7 s → 0.65 s in Chrome, 0.9 s in WebKit). The Overview's Test Values table reuses the
158
+ pooled statistics the gallery's summary panel has already computed, whether or not the lot
159
+ has functional tests, and the pass-rate chart reads each die's values and recorded verdicts
160
+ in one pass per die.
161
+ Figures are unchanged.
162
+ - **`analyzeWaferMap` is about five times faster on large wafers** (a 25-wafer, 266k-die lot:
163
+ 32 s → 6.5 s in Chrome). Region membership, die keys and cluster neighbour lookups are each
164
+ computed once per analysis, not once per finding builder. Findings are unchanged.
165
+ - **Faster building and analysis in WebKit** (the desktop app on Linux and macOS, and Safari).
166
+ Checks that asked whether a die holds any test data read the die's keys one at a time and stop
167
+ at the first; the coverage count checks bins before test data; input checking decides each
168
+ test number's legality once per wafer instead of once per die. On a 266k-die lot in WebKitGTK,
169
+ building fell from 2.5 s to 1.1 s and analysis from 14 s to 7 s. Results are unchanged.
170
+ - **A hidden Summary panel is rendered when it is opened**, not on every mount and option
171
+ change. Every gallery card has one, so a large gallery mounts faster (266k-die lot: 7.3 s →
172
+ 5.0 s) and switches plot mode faster (3.4 s → 1.4 s).
173
+ - **Test values are held as one column per test**, not as an object on each die, so a map
174
+ holds a small fraction of the memory it did: on a 266k-die, 51-test lot in Chrome, about 330 MB
175
+ where it was 1.1 GB. A result from `createWafermapWorker` moves its columns to the page without
176
+ copying them.
177
+ - **Die outlines are drawn as merged lines**, one per run of shared edges instead of four sides
178
+ per die. Galleries draw 10–17% faster in WebKit (the desktop app on Linux), where stroking each
179
+ die was most of the drawing time. Every outline is now the same weight in every browser: Chrome
180
+ drew the edge two dies share slightly darker than the wafer's outer edge, WebKit did not.
181
+
182
+ ### Added
183
+
184
+ - **`HighlightWaferTarget.dieKeysByWafer`** — a lot regional finding names its region's dies
185
+ on each counted wafer, keyed by wafer index; the gallery highlights the region on every
186
+ counted card from it.
187
+
188
+ - **`results` can be columns (`DieColumns`).** A host that already holds its results as columns
189
+ (a parser, Arrow, Parquet) passes one array per field and, for test values and verdicts, the
190
+ indices of the records that have one with their values. The map is built with no object per
191
+ record or die. Retests, derived tests, input checks and warnings behave exactly as for rows.
192
+ Missing entries are `NaN` or STDF's missing values (−32768 for coordinates, 65535 for bins and
193
+ sites). Mismatched lengths or out-of-range and repeated indices throw.
194
+
195
+ ---
196
+
197
+ ## [0.31.0] — 2026-09-25
198
+
199
+ ### Breaking
200
+
201
+ - **The 74 deprecated exports are removed**: the 73 deprecated in 0.30.0 (the low-level drawing
202
+ pipeline, the chart-data builders, the region builders and the helpers exported by accident)
203
+ and `renderFindingsReportHtml`, deprecated in 0.30.3. Each named its replacement in a console
204
+ notice. [Upgrading](https://wafertools.github.io/wafermap/upgrading/) lists every name with
205
+ what to use instead; the main replacements:
206
+
207
+ | Removed | Use instead |
208
+ |---|---|
209
+ | `resolveBinColors`, `getBinColorScheme` | `binColorsForMaps(results)`, or `getBinColors()` on a map or gallery controller |
210
+ | `valueToViridis`, `valueToGreyscale`, `getValueColorScheme` | `resolveValueColorFn(name, reversed)` |
211
+ | `buildYieldData`, `buildYieldDataCombined` | `lotYieldSeries` on `analyzeWaferLot`'s result |
212
+ | `buildBinParetoData`, `buildBinClusterData` | `stats.hardBinCounts`, `stats.softBinCounts` |
213
+ | `buildCapabilityData`, `buildTestBoxplotData`, `buildTestTrendData`, `trendCentre` | `stats.capability`, `stats.perTestStats`, `perWaferTestStats` (with `computePerTestStats`) |
214
+ | `buildTestPassRateData`, `hasJudgeableTests`, `computeFunctionalYield` | `stats.testSpecYield`, `stats.testFlagYield`, `stats.functionalYield` |
215
+ | `buildRegionYieldData`, `buildRingRegions`, `buildQuadrantRegions`, `classifyDie`, `getRingLabel` | `stats.regionYield` |
216
+ | `classifyPattern` | `stats.spatialPattern` |
217
+ | `renderSummaryReportHtml`, `renderLotSummaryReportHtml`, `renderFindingsReportHtml` | `renderWaferReportHtml(result, summary?)`, `renderLotReportHtml(results)` |
218
+ | `openHtmlReport` | `openReportModal(html)`, `setReportOpener` |
219
+ | `createWafer`, `generateDies`, `clipDiesToWafer` | `buildWaferMap({ layout: true, waferConfig, dieConfig })` |
220
+ | `aggregateValues`, `aggregateBinCounts`, `getUniqueBins` | `buildWaferMap`'s `lotStack` |
221
+ | `STANDARD_WAFER_DIAMETERS_MM`, `resolveGridPitch` | `buildWaferMap`'s `standardDiameters`; each die's `width`/`height` |
222
+ | `getDieTestValue`, `isParametricTest`, `isPositionedDie`, `metadataCategoricalValue` | `die.testValues?.[n]`, `testType !== 'F'`, `hasPosition`, `metadataDisplayValue` |
223
+
224
+ `buildView`, `toCanvas`, `buildHoverText`, `buildMapTitle`, the transform and `affine*`
225
+ helpers, the histogram, scatter and correlation data builders, the sector, reticle and
226
+ test-site region builders, `parseRegionKey`, `contrastTextColor`, `dieHasTestData`,
227
+ `resolveMetadataColumns`, `discoverDieMetadataKeys`, `buildDieListSection` and
228
+ `DEFAULT_FACET_CURATION` have no public replacement: the renderers and the analysis do that
229
+ work themselves.
230
+ - **63 types that belonged only to removed functions are removed with them**, among them the
231
+ chart-data types, the draw-list types (`View`, `ViewOptions`, `ViewRect` and the rest),
232
+ `PitchResult`, `ToCanvasResult`, `HitTarget`, `DieListOptions` and the geometry inputs
233
+ `WaferSpec`, `DieSpec` and `ReticleSpec`. Upgrading lists them all. `PlotMode` stays.
234
+ - **`buildWaferMap` takes one argument.** The second (`WaferMapOptions`) set only the starting
235
+ `result.plotMode`, which the Web Worker never passed, so a build on and off the main thread
236
+ could start in different modes. Set the starting mode with `renderWaferMap`'s or
237
+ `renderWaferGallery`'s `viewOptions.plotMode`.
238
+ - **`downloadFilename` is a prefix for every saved file**, on `renderWaferMap` and
239
+ `renderWaferGallery`: `<downloadFilename>_W05_hard-bin.png`, `<downloadFilename>_W05_die-list.csv`.
240
+ It applies to CSVs, charts, gallery cards and detached windows as well as the map's or gallery's
241
+ PNG, which it no longer names outright. Parts the prefix already names are not repeated. A host
242
+ that matched the exact name its `onSaveImage` receives should match on the prefix.
243
+ - **Values outside the STDF V4 ranges are treated as missing.** 0.30.4 reported them with the
244
+ `input-values-outside-stdf` warning and used them as given; `buildWaferMap` now leaves them out,
245
+ and the warning says what it did:
246
+ - a bin outside 0–32767 or not a whole number: the die has no bin (so it is neither pass nor fail);
247
+ - a coordinate outside ±32767 or not a whole number: the die has no position, in either axis;
248
+ - a test number outside 0–4294967295: that test is left out of every die and of `testDefs`;
249
+ - a test value that is not finite: that value is left out;
250
+ - a site number outside 0–255: the die has no site;
251
+ - a `waferConfig.orientation` other than 0, 90, 180 or 270 (or an equivalent such as −90): the
252
+ map is built at 0. `WaferConfig.orientation` is typed `0 | 90 | 180 | 270`.
253
+
254
+ The caller's input objects are not modified. A derived test whose `testNumber` is outside
255
+ 0–4294967295 is dropped with a `derived-test-invalid` warning.
256
+ - **`WaferViewOptions.showPartialDies` and `isYieldEligibleDie`'s `includePartial` are removed.**
257
+ No map `buildWaferMap` builds has partial dies, so neither had anything to act on. A saved
258
+ preference that still carries `showPartialDies` is ignored. Partial dies a host supplies
259
+ itself are drawn in muted grey and always left out of yield.
260
+
261
+ ### Added
262
+
263
+ - **Insights charts draw spec limits as well as test limits.** The boxplot, histogram,
264
+ wafer-to-wafer trend and scatter show a test's spec limits (`specLow`/`specHigh`, labelled
265
+ LSL/USL, long dashes) beside its test limits (Lo/Hi limit, short dashes). When a test has
266
+ both, a **Limits** choice (Test + spec, the default; Test limits; Spec limits; None) applies to
267
+ all four charts. Labels that would overlap move to a second row, **Axis includes limits**
268
+ covers every limit shown, and the scatter marks limits outside its plotted range at the edge,
269
+ as the other charts do. The wafer map still judges pass/fail by the test limits.
270
+ - **Chart gridlines are lighter** (the border colour at 40% opacity), so the data and the limit
271
+ lines stand out from the grid in every theme. The scatter draws its limits in the same amber
272
+ as the other charts, and its limit labels sit on a panel-coloured backing where lines cross
273
+ them.
274
+
275
+ ### Changed
276
+
277
+ - **The data-and-stats layer is ~49 KB gzipped**, from ~61 KB, with the removed exports gone.
278
+
279
+ ### Fixed
280
+
281
+ - **Charts opened from a gallery card's right-click menu are saved under the card's lot and
282
+ wafer**, as they are from a single map.
283
+
284
+ ### Documentation
285
+
286
+ - **New page, [Upgrading](https://wafertools.github.io/wafermap/upgrading/)**: what to change
287
+ for each breaking release, starting with 0.31.0.
288
+ - **Every version has a GitHub release**, created when its tag is pushed, with that version's
289
+ changelog section as its notes.
26
290
 
27
291
  ## [0.30.4] — 2026-09-25
28
292
 
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![CI and deploy](https://github.com/wafertools/wafermap/actions/workflows/deploy.yml/badge.svg)](https://github.com/wafertools/wafermap/actions/workflows/deploy.yml)
6
6
  [![npm](https://img.shields.io/npm/v/@wafertools/wafermap.svg)](https://www.npmjs.com/package/@wafertools/wafermap)
7
7
  ![runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)
8
- ![bundle](https://img.shields.io/badge/data%20layer%20min%2Bgz-~61%20kB-blue)
8
+ ![bundle](https://img.shields.io/badge/data%20layer%20min%2Bgz-~50%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 ~61 kB
16
+ Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~50 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
 
@@ -75,7 +75,7 @@ export. Add `analyzeWaferMap` for the findings and summary panel, swap in
75
75
 
76
76
  The API reference is long because it documents every option, not because you need
77
77
  them: [tsmap](https://github.com/wafertools/tsmap), a complete cross-platform desktop
78
- application built on this library, imports **17** of its ~100 exports. Read the
78
+ application built on this library, imports **16** of its ~100 exports. Read the
79
79
  [Quick Start](https://wafertools.github.io/wafermap/quickstart/) first and treat the
80
80
  [API reference](https://wafertools.github.io/wafermap/api/) as something to search,
81
81
  not to read.
@@ -1 +1 @@
1
- import{buildTestBoxplotData as nt}from"../../stats/boxplot.js";import{SPACE as Be,fontPx as Y,FONT as it,CLR as ot}from"../toolbar.js";import{fmt as he}from"../../renderer/fmt.js";import{fitTicks as lt}from"../../renderer/axisTicks.js";import{QUANTITY as He}from"./palette.js";import{cardShell as st,observeResize as at,makeTooltip as rt,positionChartTooltip as ct,makeBackButton as ft,makeLinkedTestSelect as ut,makeToggle as mt,makeLinkedAxisPrefs as dt,renderEmptyState as Re,fitRowsHeight as xt,setChartGrow as pt,resolveChartCanvasColors as ht,makeAxisFormat as De,horizontalTickSpacing as gt,resolveAxisRange as bt,shouldIncludeLimitsByDefault as Tt,drawOffAxisLimits as yt,limitLabelSide as wt,PADDING as M,VALUE_WIDTH as vt,WAFER_MENU_HINT as kt,prepareCanvas as Mt}from"./chartShell.js";import{escHtml as L,maxOf as Lt,minOf as Ct}from"../../core/utils.js";const C=24,A=5,We=110,At=12,Oe=20;export function renderBoxplotPanel(u){const{title:ee="Test value distribution",items:Ee,testDefs:ge,onSaveImage:Pe,groups:g,groupLabelText:te="group",onOpen:z}=u,_e=u.openActionLabel??"open that wafer",Ge=u.openActionLabel??"open this wafer",{card:a,heading:Ne,body:x,controlsRow:Q}=st(ee,Pe,u.ownerDocument);pt(a,"rows"),a.style.flex="0 0 auto",x.style.flex="0 0 auto",a.style.alignSelf="start";const ne=ge.filter(i=>i.testNumber!==void 0);let y=u.selectedTestNumber??ne[0]?.testNumber??null,ie=!1,U=0,b=null,R=null,oe=null,be=null;function Xe(){const i=!g||g.length===0?"":b??"\0overview";if(oe&&be===i)return oe;const p=!g||g.length===0?Ee:b!==null?g.find(h=>h.key===b)?.items??[]:g.map(h=>({label:h.key,dies:h.items.flatMap(f=>f.dies??[])}));return oe=p,be=i,p}function Te(){b!==null&&!R?(R=ft(()=>{ye(null),u.onGroupChange?.(null)},a.ownerDocument),Q.appendChild(R)):b===null&&R&&(R.remove(),R=null),Ne.textContent=b!==null?`${ee} \u2014 ${te}: ${b}`:ee}function ye(i){return b===i||i!==null&&!(g??[]).some(p=>p.key===i)?!1:(b=i,Te(),F(),$(),!0)}const we=ut(ne,y,i=>{y=i,$(),u.onTestChange?.(i)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),qe=we.el;Q.appendChild(qe),Q.appendChild(mt("Log scale",ie,i=>{ie=i,$()},a.ownerDocument));const le=a.ownerDocument.createElement("span");Object.assign(le.style,{display:"inline-flex",gap:Be.lg,alignItems:"center"}),Q.appendChild(le);const se=dt(le,u.axisPrefs,i=>{u.onAxisPrefsChange?.(i),$()},a.ownerDocument),ae=a.ownerDocument.createElement("div");Object.assign(ae.style,{color:ot.label,fontSize:it.body,marginBottom:Be.sm}),a.insertBefore(ae,x);function F(){const i=!!g&&g.length>0&&b===null,p=[];i?p.push(`click a ${te}'s box to see it by wafer`):z&&p.push(`click a box to ${_e}`);const h=p.length?`${p[0][0].toUpperCase()}${p[0].slice(1)} \xB7 `:"";ae.textContent=`${h}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(U?` \xB7 axis clipped, ${U} value${U===1?"":"s"} outside`:"")}F(),Te();const D=rt(a);let re=null;function $(){const{includeLimits:i,clipOutliers:p}=se.get();if(F(),x.innerHTML="",ne.length===0||y===null){Re(x,"No parametric test data available for box plots.");return}const h=Xe(),f=nt(h,y),ce=ge.find(t=>t.testNumber===y),W=ce?.unit,V=ce?.limitLow,j=ce?.limitHigh;if(f.every(t=>t.count===0)){Re(x,"No parametric test data available for box plots.");return}const m=a.ownerDocument.createElement("canvas");m.style.display="block",m.style.cursor="default",x.appendChild(m);const ve=M*2+Math.min(f.length,At)*(C+A)+Oe;x.style.maxHeight=`${ve}px`,x.style.overflowY="auto",x.style.scrollbarGutter="stable";let P=-1;const fe=f.filter(t=>t.count>0),ke=Ct(fe.map(t=>t.min)),Me=Lt(fe.map(t=>t.max)),Le=i??Tt(ke,Me,V,j);se.sync(Le,V!==void 0||j!==void 0);const _=bt({dataMin:ke,dataMax:Me,limitLow:V,limitHigh:j,includeLimits:Le,clipOutliers:p,values:fe.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});U=_.clippedCount,F();const O=_.lo,K=_.hi,Qe=K-O||1,J=ie&&O>0,ue=J?Math.log10(O):0,Ce=(J?Math.log10(K):0)-ue||1,me=Math.max(Math.abs(O),Math.abs(K));function Ue(){const t=M+We,s=m.clientWidth-t-vt-M;return{plotX:t,plotMaxWidth:Math.max(10,s)}}function S(t,s,n){return J?s+(Math.log10(t)-ue)/Ce*n:s+(t-O)/Qe*n}function Fe(t,s){if(J){const d=[];for(let e=0;e<=4;e++)d.push(Math.pow(10,ue+Ce*e/4));return{ticks:d,axis:De(me,W)}}const n=lt(O,K,s,gt(t,me,W));return{ticks:n.ticks,axis:De(me,W,n.step||void 0)}}function Z(){const t=ht(a),s=M*2+f.length*(C+A)+Oe;x.style.maxHeight=`${xt(a,x,ve,s)}px`;const n=x.clientWidth,d=Mt(m,a,n,s);if(!d)return;const{ctx:e}=d;e.font=`${Y(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:r}=Ue();f.forEach((o,T)=>{const v=M+T*(C+A),c=v+C/2;T===P&&(e.fillStyle=t.bgHover,e.fillRect(0,v-A/2,n,C+A)),e.fillStyle=t.text,e.textAlign="right";const Ke=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(Ke,M+We-8,c),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,c);return}const Je=l+r,N=tt=>Math.min(Je,Math.max(l,tt)),$e=S(o.min,l,r),Se=S(o.max,l,r),I=N($e),X=N(S(o.q1,l,r)),Ie=N(S(o.median,l,r)),pe=N(S(o.q3,l,r)),B=N(Se),Ze=$e<I,et=Se>B,H=v+3,q=v+C-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(I,c),e.lineTo(X,c),e.moveTo(pe,c),e.lineTo(B,c);const k=3;Ze?(e.moveTo(I+k,c-k),e.lineTo(I,c),e.lineTo(I+k,c+k)):(e.moveTo(I,H),e.lineTo(I,q)),et?(e.moveTo(B-k,c-k),e.lineTo(B,c),e.lineTo(B-k,c+k)):(e.moveTo(B,H),e.lineTo(B,q)),e.stroke(),e.fillStyle=He,e.globalAlpha=.35,e.fillRect(X,H,Math.max(1,pe-X),q-H),e.globalAlpha=1,e.strokeStyle=He,e.strokeRect(X,H,Math.max(1,pe-X),q-H),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Ie,H),e.lineTo(Ie,q),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(he(o.median,W,"engineering"),l+r+10,c)});const w=M+f.length*(C+A);e.font=`${Y(-1)}px system-ui, sans-serif`,yt(e,_.offAxis,{left:l,right:l+r,top:0,bottom:w},"horizontal",t.limitLine,o=>he(o,W,"engineering"));const Ve=new Set(_.offAxis.map(o=>o.value));for(const[o,T]of[[V,"Lo limit"],[j,"Hi limit"]]){if(o===void 0||Ve.has(o))continue;const v=S(o,l,r);e.strokeStyle=t.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(v,0),e.lineTo(v,w),e.stroke(),e.setLineDash([]),e.fillStyle=t.limitLine;const c=wt(v,e.measureText(T).width,l,l+r,T==="Lo limit");e.textAlign=c<0?"right":"left",e.textBaseline="bottom",e.fillText(T,v+c*3,w-1)}e.font=`${Y(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,w),e.lineTo(l+r,w),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:je,axis:xe}=Fe(e,r);for(const o of je){const T=S(o,l,r);e.beginPath(),e.moveTo(T,w),e.lineTo(T,w+4),e.stroke(),e.fillText(xe.tick(o),T,w+6)}xe.unitLabel&&(e.textAlign="left",e.font=`${Y(-1)}px system-ui, sans-serif`,e.fillText(`(${xe.unitLabel})`,l+r+22,w+6),e.font=`${Y(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function de(t){const s=Math.floor((t-M+A/2)/(C+A));return s>=0&&s<f.length?s:-1}function G(t){return he(t,W,"engineering")}const E=!!g&&g.length>0&&b===null,Ae=t=>!E&&!!z&&h[t]?.waferIndex!==void 0;m.addEventListener("mousemove",t=>{const s=m.getBoundingClientRect(),n=de(t.clientY-s.top),d=n>=0&&f[n].count>0&&(E||Ae(n));if(n!==P&&(P=n,m.style.cursor=d?"pointer":"default",Z()),n>=0&&f[n].count>0){const e=f[n],l=E?`<br><em>click to see this ${L(te)} by wafer</em>`:Ae(n)?`<br><em>click to ${L(Ge)}</em>`:"",r=!E&&u.onWaferContextMenu&&h[n]?.waferIndex!==void 0?kt:"";D.innerHTML=`<strong>${L(e.label)}</strong> (${e.count} dies)<br>max ${L(G(e.max))}<br>q3 ${L(G(e.q3))}<br>median ${L(G(e.median))}<br>q1 ${L(G(e.q1))}<br>min ${L(G(e.min))}${l}${r}`,D.style.display="block",ct(D,a,t.clientX,t.clientY)}else D.style.display="none"}),m.addEventListener("mouseleave",()=>{P!==-1&&(P=-1,Z()),D.style.display="none"}),m.addEventListener("click",t=>{const s=m.getBoundingClientRect(),n=de(t.clientY-s.top);if(n===-1||f[n].count===0)return;if(E){const e=f[n].label;ye(e),u.onGroupChange?.(e);return}const d=h[n]?.waferIndex;z&&d!==void 0&&y!==null&&z(d,y)}),m.addEventListener("contextmenu",t=>{const s=m.getBoundingClientRect(),n=de(t.clientY-s.top),d=n===-1?void 0:h[n]?.waferIndex;E||d===void 0||!u.onWaferContextMenu||(D.style.display="none",u.onWaferContextMenu(d,y??void 0,t))}),re?.disconnect(),re=at(a,()=>Z()),Z()}$();function Ye(i){we.set(i)&&(y=i,$())}function ze(i){se.set(i)&&$()}return{card:a,setTest:Ye,setAxisPrefs:ze,destroy:()=>re?.disconnect()}}
1
+ import{buildTestBoxplotData as rt}from"../../stats/boxplot.js";import{SPACE as He,fontPx as _,FONT as ct,CLR as ft}from"../toolbar.js";import{fmt as ge}from"../../renderer/fmt.js";import{fitTicks as ut}from"../../renderer/axisTicks.js";import{QUANTITY as De}from"./palette.js";import{cardShell as mt,observeResize as dt,makeTooltip as xt,positionChartTooltip as pt,makeBackButton as ht,makeLinkedTestSelect as gt,makeToggle as bt,makeLinkedAxisPrefs as yt,renderEmptyState as Ee,fitRowsHeight as Tt,setChartGrow as wt,resolveChartCanvasColors as vt,makeAxisFormat as Oe,horizontalTickSpacing as kt,resolveAxisRange as Mt,shouldIncludeLimitsByDefault as Lt,drawOffAxisLimits as Ct,limitLabelSide as At,limitLines as We,limitExtent as $t,hasBothLimitKinds as St,stackLabelRows as It,strokeLimitLine as Bt,PADDING as M,VALUE_WIDTH as Rt,WAFER_MENU_HINT as Ht,prepareCanvas as Dt}from"./chartShell.js";import{escHtml as L,maxOf as Et,minOf as Ot}from"../../core/utils.js";const C=24,A=5,Pe=110,Wt=12,_e=20;export function renderBoxplotPanel(u){const{title:te="Test value distribution",items:Ge,testDefs:be,onSaveImage:Ne,groups:g,groupLabelText:ne="group",onOpen:Q}=u,Xe=u.openActionLabel??"open that wafer",qe=u.openActionLabel??"open this wafer",{card:a,heading:Ye,body:x,controlsRow:U}=mt(te,Ne,u.ownerDocument);wt(a,"rows"),a.style.flex="0 0 auto",x.style.flex="0 0 auto",a.style.alignSelf="start";const oe=be.filter(i=>i.testNumber!==void 0);let T=u.selectedTestNumber??oe[0]?.testNumber??null,ie=!1,F=0,b=null,D=null,le=null,ye=null;function ze(){const i=!g||g.length===0?"":b??"\0overview";if(le&&ye===i)return le;const p=!g||g.length===0?Ge:b!==null?g.find(v=>v.key===b)?.items??[]:g.map(v=>({label:v.key,dies:v.items.flatMap(S=>S.dies??[])}));return le=p,ye=i,p}function Te(){b!==null&&!D?(D=ht(()=>{we(null),u.onGroupChange?.(null)},a.ownerDocument),U.appendChild(D)):b===null&&D&&(D.remove(),D=null),Ye.textContent=b!==null?`${te} \u2014 ${ne}: ${b}`:te}function we(i){return b===i||i!==null&&!(g??[]).some(p=>p.key===i)?!1:(b=i,Te(),V(),$(),!0)}const ve=gt(oe,T,i=>{T=i,$(),u.onTestChange?.(i)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Qe=ve.el;U.appendChild(Qe),U.appendChild(bt("Log scale",ie,i=>{ie=i,$()},a.ownerDocument));const se=a.ownerDocument.createElement("span");Object.assign(se.style,{display:"inline-flex",gap:He.lg,alignItems:"center"}),U.appendChild(se);const ae=yt(se,u.axisPrefs,i=>{u.onAxisPrefsChange?.(i),$()},a.ownerDocument),re=a.ownerDocument.createElement("div");Object.assign(re.style,{color:ft.label,fontSize:ct.body,marginBottom:He.sm}),a.insertBefore(re,x);function V(){const i=!!g&&g.length>0&&b===null,p=[];i?p.push(`click a ${ne}'s box to see it by wafer`):Q&&p.push(`click a box to ${Xe}`);const v=p.length?`${p[0][0].toUpperCase()}${p[0].slice(1)} \xB7 `:"";re.textContent=`${v}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(F?` \xB7 axis clipped, ${F} value${F===1?"":"s"} outside`:"")}V(),Te();const E=xt(a);let ce=null;function $(){const{includeLimits:i,clipOutliers:p,limits:v}=ae.get();if(V(),x.innerHTML="",oe.length===0||T===null){Ee(x,"No parametric test data available for box plots.");return}const S=ze(),h=rt(S,T),j=be.find(t=>t.testNumber===T),O=j?.unit,fe=We(j,v),{lo:Ve,hi:je}=$t(fe);if(h.every(t=>t.count===0)){Ee(x,"No parametric test data available for box plots.");return}const m=a.ownerDocument.createElement("canvas");m.style.display="block",m.style.cursor="default",x.appendChild(m);const ke=M*2+Math.min(h.length,Wt)*(C+A)+_e;x.style.maxHeight=`${ke}px`,x.style.overflowY="auto",x.style.scrollbarGutter="stable";let G=-1;const ue=h.filter(t=>t.count>0),Me=Ot(ue.map(t=>t.min)),Le=Et(ue.map(t=>t.max)),Ce=i??Lt(Me,Le,Ve,je);ae.sync(Ce,We(j).length>0,St(j));const N=Mt({dataMin:Me,dataMax:Le,limits:fe,includeLimits:Ce,clipOutliers:p,values:ue.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});F=N.clippedCount,V();const W=N.lo,K=N.hi,Ke=K-W||1,J=ie&&W>0,me=J?Math.log10(W):0,Ae=(J?Math.log10(K):0)-me||1,de=Math.max(Math.abs(W),Math.abs(K));function Je(){const t=M+Pe,s=m.clientWidth-t-Rt-M;return{plotX:t,plotMaxWidth:Math.max(10,s)}}function I(t,s,n){return J?s+(Math.log10(t)-me)/Ae*n:s+(t-W)/Ke*n}function Ze(t,s){if(J){const d=[];for(let e=0;e<=4;e++)d.push(Math.pow(10,me+Ae*e/4));return{ticks:d,axis:Oe(de,O)}}const n=ut(W,K,s,kt(t,de,O));return{ticks:n.ticks,axis:Oe(de,O,n.step||void 0)}}function Z(){const t=vt(a),s=M*2+h.length*(C+A)+_e;x.style.maxHeight=`${Tt(a,x,ke,s)}px`;const n=x.clientWidth,d=Dt(m,a,n,s);if(!d)return;const{ctx:e}=d;e.font=`${_(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:c}=Je();h.forEach((o,f)=>{const y=M+f*(C+A),r=y+C/2;f===G&&(e.fillStyle=t.bgHover,e.fillRect(0,y-A/2,n,C+A)),e.fillStyle=t.text,e.textAlign="right";const ee=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(ee,M+Pe-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const it=l+c,q=at=>Math.min(it,Math.max(l,at)),Ie=I(o.min,l,c),Be=I(o.max,l,c),B=q(Ie),Y=q(I(o.q1,l,c)),Re=q(I(o.median,l,c)),he=q(I(o.q3,l,c)),R=q(Be),lt=Ie<B,st=Be>R,H=y+3,z=y+C-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(B,r),e.lineTo(Y,r),e.moveTo(he,r),e.lineTo(R,r);const k=3;lt?(e.moveTo(B+k,r-k),e.lineTo(B,r),e.lineTo(B+k,r+k)):(e.moveTo(B,H),e.lineTo(B,z)),st?(e.moveTo(R-k,r-k),e.lineTo(R,r),e.lineTo(R-k,r+k)):(e.moveTo(R,H),e.lineTo(R,z)),e.stroke(),e.fillStyle=De,e.globalAlpha=.35,e.fillRect(Y,H,Math.max(1,he-Y),z-H),e.globalAlpha=1,e.strokeStyle=De,e.strokeRect(Y,H,Math.max(1,he-Y),z-H),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Re,H),e.lineTo(Re,z),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(ge(o.median,O,"engineering"),l+c+10,r)});const w=M+h.length*(C+A);e.font=`${_(-1)}px system-ui, sans-serif`,Ct(e,N.offAxis,{left:l,right:l+c,top:0,bottom:w},"horizontal",t.limitLine,o=>ge(o,O,"engineering"));const et=new Set(N.offAxis.map(o=>o.value)),Se=fe.filter(o=>!et.has(o.value)).map(o=>{const f=I(o.value,l,c),y=e.measureText(o.label).width,r=At(f,y,l,l+c,o.end==="lo"),ee=r<0?f-3-y:f+3;return{l:o,x:f,dir:r,start:ee,end:ee+y}}),tt=It(Se),nt=_(-1)+2;Se.forEach(({l:o,x:f,dir:y},r)=>{Bt(e,o,t.limitLine,f,0,f,w),e.fillStyle=t.limitLine,e.textAlign=y<0?"right":"left",e.textBaseline="bottom",e.fillText(o.label,f+y*3,w-1-tt[r]*nt)}),e.font=`${_(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,w),e.lineTo(l+c,w),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:ot,axis:pe}=Ze(e,c);for(const o of ot){const f=I(o,l,c);e.beginPath(),e.moveTo(f,w),e.lineTo(f,w+4),e.stroke(),e.fillText(pe.tick(o),f,w+6)}pe.unitLabel&&(e.textAlign="left",e.font=`${_(-1)}px system-ui, sans-serif`,e.fillText(`(${pe.unitLabel})`,l+c+22,w+6),e.font=`${_(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function xe(t){const s=Math.floor((t-M+A/2)/(C+A));return s>=0&&s<h.length?s:-1}function X(t){return ge(t,O,"engineering")}const P=!!g&&g.length>0&&b===null,$e=t=>!P&&!!Q&&S[t]?.waferIndex!==void 0;m.addEventListener("mousemove",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top),d=n>=0&&h[n].count>0&&(P||$e(n));if(n!==G&&(G=n,m.style.cursor=d?"pointer":"default",Z()),n>=0&&h[n].count>0){const e=h[n],l=P?`<br><em>click to see this ${L(ne)} by wafer</em>`:$e(n)?`<br><em>click to ${L(qe)}</em>`:"",c=!P&&u.onWaferContextMenu&&S[n]?.waferIndex!==void 0?Ht:"";E.innerHTML=`<strong>${L(e.label)}</strong> (${e.count} dies)<br>max ${L(X(e.max))}<br>q3 ${L(X(e.q3))}<br>median ${L(X(e.median))}<br>q1 ${L(X(e.q1))}<br>min ${L(X(e.min))}${l}${c}`,E.style.display="block",pt(E,a,t.clientX,t.clientY)}else E.style.display="none"}),m.addEventListener("mouseleave",()=>{G!==-1&&(G=-1,Z()),E.style.display="none"}),m.addEventListener("click",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top);if(n===-1||h[n].count===0)return;if(P){const e=h[n].label;we(e),u.onGroupChange?.(e);return}const d=S[n]?.waferIndex;Q&&d!==void 0&&T!==null&&Q(d,T)}),m.addEventListener("contextmenu",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top),d=n===-1?void 0:S[n]?.waferIndex;P||d===void 0||!u.onWaferContextMenu||(E.style.display="none",u.onWaferContextMenu(d,T??void 0,t))}),ce?.disconnect(),ce=dt(a,()=>Z()),Z()}$();function Ue(i){ve.set(i)&&(T=i,$())}function Fe(i){ae.set(i)&&$()}return{card:a,setTest:Ue,setAxisPrefs:Fe,destroy:()=>ce?.disconnect()}}
@@ -43,7 +43,20 @@ export interface ChartCanvasColors {
43
43
  * limit line and its label need.
44
44
  */
45
45
  limitLine: string;
46
+ /**
47
+ * Gridlines: `border` at `GRID_ALPHA`. A gridline is a reading aid behind the
48
+ * data; drawn at full `border` strength (the axis colour) it competed with the
49
+ * points and with the limit lines, which in a theme with a strong border were
50
+ * hard to pick out from the grid at all.
51
+ */
52
+ grid: string;
46
53
  }
54
+ /** Opacity of gridlines relative to the border colour. */
55
+ export declare const GRID_ALPHA = 0.4;
56
+ /** `color` at `alpha` × its own opacity. Understands the forms a computed
57
+ * custom property holds — `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()` —
58
+ * and returns anything else unchanged rather than guessing. */
59
+ export declare function withAlpha(color: string, alpha: number): string;
47
60
  /** Resolve `--wmap-*` custom properties to concrete color strings for canvas
48
61
  * drawing. Call once per draw (not cached) so a live theme change is picked
49
62
  * up on the next redraw, matching `resolveCanvasTheme`'s own contract. */
@@ -349,6 +362,7 @@ export interface LinkedAxisPrefs {
349
362
  get(): {
350
363
  includeLimits: boolean | undefined;
351
364
  clipOutliers: boolean;
365
+ limits: LimitsShown;
352
366
  };
353
367
  /**
354
368
  * Rebuild the toggles for the state the panel has just RESOLVED.
@@ -356,9 +370,10 @@ export interface LinkedAxisPrefs {
356
370
  * Called from the redraw, because `includeLimits` is a tri-state: undefined
357
371
  * means "decide from the data" (`shouldIncludeLimitsByDefault`), and the
358
372
  * resolved answer is only known once the data range is. `hasLimits` decides
359
- * whether the limits toggle is offered at all.
373
+ * whether the limits toggle is offered at all, and `hasBothKinds` whether the
374
+ * "Limits:" choice between test and spec limits is.
360
375
  */
361
- sync(resolvedIncludeLimits: boolean, hasLimits: boolean): void;
376
+ sync(resolvedIncludeLimits: boolean, hasLimits: boolean, hasBothKinds?: boolean): void;
362
377
  /**
363
378
  * Adopt prefs chosen elsewhere. Returns false — changing nothing — when they
364
379
  * already match. Deliberately does NOT fire `onUserChange`, the same
@@ -490,7 +505,72 @@ export declare function positionChartTooltip(tooltip: HTMLElement, card: HTMLEle
490
505
  export interface AxisPrefs {
491
506
  includeLimits?: boolean;
492
507
  clipOutliers: boolean;
508
+ /** Which limits the charts draw. Undefined = `'both'`. */
509
+ limits?: LimitsShown;
493
510
  }
511
+ export type LimitKind = 'test' | 'spec';
512
+ /** The Insights "Limits" choice. */
513
+ export type LimitsShown = 'test' | 'spec' | 'both' | 'none';
514
+ export interface LimitLine {
515
+ value: number;
516
+ kind: LimitKind;
517
+ /** Which end of its pair: the low limit or the high one. */
518
+ end: 'lo' | 'hi';
519
+ label: string;
520
+ }
521
+ export declare const LIMIT_LABEL: Readonly<Record<LimitKind, {
522
+ lo: string;
523
+ hi: string;
524
+ }>>;
525
+ /** Canvas dash pattern per kind: short dashes for test limits, long for spec. */
526
+ export declare const LIMIT_DASH: Readonly<Record<LimitKind, number[]>>;
527
+ export declare const LIMITS_SHOWN_OPTIONS: ReadonlyArray<{
528
+ value: LimitsShown;
529
+ label: string;
530
+ }>;
531
+ type LimitSource = {
532
+ limitLow?: number;
533
+ limitHigh?: number;
534
+ specLow?: number;
535
+ specHigh?: number;
536
+ } | undefined;
537
+ /** Whether a test has both kinds of limit — the only case with a choice to make. */
538
+ export declare function hasBothLimitKinds(def: LimitSource): boolean;
539
+ /**
540
+ * The limit lines to draw for a test.
541
+ *
542
+ * `'none'` draws nothing. Otherwise the chosen kinds are drawn — and when the
543
+ * test has none of the chosen kind but does have the other, the other is drawn
544
+ * rather than nothing: a test with only test limits must not look unlimited
545
+ * because another test's spec limits were chosen. Each line carries its own
546
+ * label, so which kind is on screen is never in doubt.
547
+ */
548
+ export declare function limitLines(def: LimitSource, shown?: LimitsShown): LimitLine[];
549
+ /** The span the lines cover, for axis-range decisions. */
550
+ export declare function limitExtent(lines: readonly LimitLine[]): {
551
+ lo?: number;
552
+ hi?: number;
553
+ };
554
+ /**
555
+ * Stack labels into rows so none overlaps another in the same row. Each label
556
+ * is an interval along the axis it is placed on; returns a row index per label
557
+ * (0 = the chart's usual label position), in input order. Greedy by position,
558
+ * so it is stable: labels that never collide all stay on row 0.
559
+ */
560
+ export declare function stackLabelRows(spans: ReadonlyArray<{
561
+ start: number;
562
+ end: number;
563
+ }>, gap?: number): number[];
564
+ /**
565
+ * `fillText` on a backing of `halo` (the panel colour), so a label stays
566
+ * readable where a gridline, a limit line or data runs under it. Uses the
567
+ * current font, alignment and baseline; `halo` undefined draws plain text.
568
+ */
569
+ export declare function fillTextOnHalo(ctx: CanvasRenderingContext2D, text: string, x: number, y: number, halo?: string): void;
570
+ /** Stroke one limit line in its kind's dash pattern. */
571
+ export declare function strokeLimitLine(ctx: CanvasRenderingContext2D, line: LimitLine, color: string, x0: number, y0: number, x1: number, y1: number): void;
572
+ /** The "Limits:" dropdown, offered only where a test has both kinds. */
573
+ export declare function makeLimitsSelect(current: LimitsShown, onChange: (v: LimitsShown) => void, ownerDocument?: Document): HTMLLabelElement;
494
574
  /** Robust outlier fence over a value list: Tukey's `Q1 − k·IQR … Q3 + k·IQR`.
495
575
  *
496
576
  * Deliberately NOT mean ± 3σ. σ is computed FROM the data including the
@@ -524,7 +604,7 @@ export interface AxisRange {
524
604
  * is merely off-screen is indistinguishable from a test having no limit. */
525
605
  offAxis: Array<{
526
606
  value: number;
527
- label: 'Lo limit' | 'Hi limit';
607
+ label: string;
528
608
  side: 'lo' | 'hi';
529
609
  }>;
530
610
  /** Values excluded by the robust fence, when clipping is on. */
@@ -542,8 +622,8 @@ export interface AxisRange {
542
622
  export declare function resolveAxisRange(opts: {
543
623
  dataMin: number;
544
624
  dataMax: number;
545
- limitLow?: number;
546
- limitHigh?: number;
625
+ /** The limit lines the chart draws (`limitLines`). */
626
+ limits?: readonly LimitLine[];
547
627
  includeLimits: boolean;
548
628
  clipOutliers?: boolean;
549
629
  /** Raw values, needed only when `clipOutliers` is set. */
@@ -580,5 +660,8 @@ export declare function drawOffAxisLimits(ctx: CanvasRenderingContext2D, offAxis
580
660
  right: number;
581
661
  top: number;
582
662
  bottom: number;
583
- }, orient: 'horizontal' | 'vertical', color: string, format: (v: number) => string): void;
663
+ }, orient: 'horizontal' | 'vertical', color: string, format: (v: number) => string,
664
+ /** Panel colour to back each marker with (`fillTextOnHalo`), for charts
665
+ * where lines can run under the markers. */
666
+ halo?: string): void;
584
667
  //# sourceMappingURL=chartShell.d.ts.map