@wafertools/wafermap 0.32.0 → 0.34.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 (167) hide show
  1. package/AGENTS.md +7 -7
  2. package/CHANGELOG.md +374 -1
  3. package/README.md +5 -5
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +15 -10
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +7 -0
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/binCluster.d.ts +7 -1
  9. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +57 -15
  13. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/correlation.js +1 -2
  15. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +2 -0
  16. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/histogram.d.ts +16 -0
  18. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  19. package/dist/packages/canvas-adapter/charts/palette.d.ts +2 -0
  20. package/dist/packages/canvas-adapter/charts/palette.js +1 -1
  21. package/dist/packages/canvas-adapter/charts/plotChart.d.ts +27 -0
  22. package/dist/packages/canvas-adapter/charts/plotChart.js +1 -0
  23. package/dist/packages/canvas-adapter/charts/pointInteractions.d.ts +45 -0
  24. package/dist/packages/canvas-adapter/charts/pointInteractions.js +1 -0
  25. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +2 -0
  26. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  27. package/dist/packages/canvas-adapter/charts/scatter.d.ts +19 -0
  28. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  29. package/dist/packages/canvas-adapter/charts/sweep.d.ts +12 -0
  30. package/dist/packages/canvas-adapter/charts/sweep.js +1 -1
  31. package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +7 -0
  32. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  33. package/dist/packages/canvas-adapter/charts/trend.d.ts +11 -0
  34. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  35. package/dist/packages/canvas-adapter/confirmDialog.d.ts +13 -0
  36. package/dist/packages/canvas-adapter/confirmDialog.js +1 -0
  37. package/dist/packages/canvas-adapter/dataTab.d.ts +131 -0
  38. package/dist/packages/canvas-adapter/dataTab.js +1 -0
  39. package/dist/packages/canvas-adapter/dieList.d.ts +39 -4
  40. package/dist/packages/canvas-adapter/dieList.js +6 -7
  41. package/dist/packages/canvas-adapter/drilldown.d.ts +20 -3
  42. package/dist/packages/canvas-adapter/drilldown.js +1 -1
  43. package/dist/packages/canvas-adapter/guideWindow.d.ts +96 -0
  44. package/dist/packages/canvas-adapter/guideWindow.js +1 -0
  45. package/dist/packages/canvas-adapter/identityHeader.d.ts +16 -0
  46. package/dist/packages/canvas-adapter/identityHeader.js +1 -1
  47. package/dist/packages/canvas-adapter/index.d.ts +3 -3
  48. package/dist/packages/canvas-adapter/index.js +1 -1
  49. package/dist/packages/canvas-adapter/insightsTab.d.ts +73 -54
  50. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  51. package/dist/packages/canvas-adapter/maplessSummary.d.ts +2 -2
  52. package/dist/packages/canvas-adapter/plotEditor.d.ts +29 -0
  53. package/dist/packages/canvas-adapter/plotEditor.js +1 -0
  54. package/dist/packages/canvas-adapter/plotItems.d.ts +32 -0
  55. package/dist/packages/canvas-adapter/plotItems.js +1 -0
  56. package/dist/packages/canvas-adapter/plotModal.d.ts +30 -0
  57. package/dist/packages/canvas-adapter/plotModal.js +1 -0
  58. package/dist/packages/canvas-adapter/plotStore.d.ts +52 -0
  59. package/dist/packages/canvas-adapter/plotStore.js +1 -0
  60. package/dist/packages/canvas-adapter/plotTab.d.ts +30 -0
  61. package/dist/packages/canvas-adapter/plotTab.js +1 -0
  62. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +3 -2
  63. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  64. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +51 -2
  65. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  66. package/dist/packages/canvas-adapter/summaryPanel.d.ts +49 -11
  67. package/dist/packages/canvas-adapter/summaryPanel.js +2 -3
  68. package/dist/packages/canvas-adapter/summaryRail.d.ts +7 -0
  69. package/dist/packages/canvas-adapter/summaryRail.js +1 -0
  70. package/dist/packages/canvas-adapter/sweepEditor.d.ts +17 -0
  71. package/dist/packages/canvas-adapter/sweepEditor.js +1 -0
  72. package/dist/packages/canvas-adapter/tableExport.d.ts +22 -0
  73. package/dist/packages/canvas-adapter/tableExport.js +3 -0
  74. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  75. package/dist/packages/canvas-adapter/toolbar.d.ts +74 -93
  76. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  77. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  78. package/dist/packages/canvas-adapter/userGuideHtml.js +409 -51
  79. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  80. package/dist/packages/canvas-adapter/version.js +1 -1
  81. package/dist/packages/canvas-adapter/viewSwitch.d.ts +13 -0
  82. package/dist/packages/canvas-adapter/viewSwitch.js +1 -0
  83. package/dist/packages/canvas-adapter/virtualTable.d.ts +44 -0
  84. package/dist/packages/canvas-adapter/virtualTable.js +1 -0
  85. package/dist/packages/core/aggregates.d.ts +6 -0
  86. package/dist/packages/core/aggregates.js +1 -1
  87. package/dist/packages/core/classify.d.ts +3 -2
  88. package/dist/packages/core/classify.js +1 -1
  89. package/dist/packages/core/compact.d.ts +205 -0
  90. package/dist/packages/core/compact.js +3 -0
  91. package/dist/packages/core/passBins.d.ts +23 -4
  92. package/dist/packages/core/passBins.js +1 -1
  93. package/dist/packages/core/ringCount.d.ts +22 -0
  94. package/dist/packages/core/ringCount.js +1 -0
  95. package/dist/packages/core/tableCsv.d.ts +43 -0
  96. package/dist/packages/core/tableCsv.js +3 -0
  97. package/dist/packages/renderer/axisTicks.d.ts +14 -0
  98. package/dist/packages/renderer/axisTicks.js +1 -1
  99. package/dist/packages/renderer/binColors.d.ts +6 -6
  100. package/dist/packages/renderer/binColors.js +1 -1
  101. package/dist/packages/renderer/buildView.d.ts +27 -2
  102. package/dist/packages/renderer/buildView.js +1 -1
  103. package/dist/packages/renderer/buildWaferMap.js +1 -1
  104. package/dist/packages/renderer/compactView.d.ts +22 -0
  105. package/dist/packages/renderer/compactView.js +1 -0
  106. package/dist/packages/renderer/xyIndicator.d.ts +27 -0
  107. package/dist/packages/renderer/xyIndicator.js +1 -0
  108. package/dist/packages/stats/analyzeWaferLot.d.ts +28 -0
  109. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  110. package/dist/packages/stats/analyzeWaferMap.d.ts +36 -1
  111. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  112. package/dist/packages/stats/binPareto.d.ts +1 -1
  113. package/dist/packages/stats/binPareto.js +1 -1
  114. package/dist/packages/stats/binRows.d.ts +38 -0
  115. package/dist/packages/stats/binRows.js +1 -0
  116. package/dist/packages/stats/filterFindings.d.ts +25 -0
  117. package/dist/packages/stats/filterFindings.js +1 -1
  118. package/dist/packages/stats/findingText.d.ts +9 -0
  119. package/dist/packages/stats/findingText.js +2 -0
  120. package/dist/packages/stats/histogram.d.ts +27 -0
  121. package/dist/packages/stats/histogram.js +1 -1
  122. package/dist/packages/stats/index.d.ts +2 -0
  123. package/dist/packages/stats/index.js +1 -1
  124. package/dist/packages/stats/lotDrift.d.ts +16 -0
  125. package/dist/packages/stats/lotDrift.js +1 -0
  126. package/dist/packages/stats/lotPattern.d.ts +33 -0
  127. package/dist/packages/stats/lotPattern.js +1 -0
  128. package/dist/packages/stats/math.d.ts +60 -6
  129. package/dist/packages/stats/math.js +1 -1
  130. package/dist/packages/stats/patternClassification.d.ts +32 -1
  131. package/dist/packages/stats/patternClassification.js +1 -1
  132. package/dist/packages/stats/plotData.d.ts +258 -0
  133. package/dist/packages/stats/plotData.js +1 -0
  134. package/dist/packages/stats/plotId.d.ts +3 -0
  135. package/dist/packages/stats/plotId.js +1 -0
  136. package/dist/packages/stats/plotSpec.d.ts +119 -0
  137. package/dist/packages/stats/plotSpec.js +4 -0
  138. package/dist/packages/stats/presentation.d.ts +44 -0
  139. package/dist/packages/stats/presentation.js +1 -0
  140. package/dist/packages/stats/regions.d.ts +15 -0
  141. package/dist/packages/stats/regions.js +1 -1
  142. package/dist/packages/stats/renderSummaryReport.d.ts +23 -5
  143. package/dist/packages/stats/renderSummaryReport.js +32 -27
  144. package/dist/packages/stats/reportHtml.d.ts +54 -9
  145. package/dist/packages/stats/reportHtml.js +145 -55
  146. package/dist/packages/stats/scatter.d.ts +6 -0
  147. package/dist/packages/stats/scatter.js +1 -1
  148. package/dist/packages/stats/sweep.d.ts +37 -1
  149. package/dist/packages/stats/sweep.js +1 -1
  150. package/dist/packages/stats/sweepText.d.ts +14 -0
  151. package/dist/packages/stats/sweepText.js +1 -0
  152. package/dist/packages/stats/sweepXFromName.d.ts +14 -0
  153. package/dist/packages/stats/sweepXFromName.js +1 -1
  154. package/dist/packages/stats/synthesis.d.ts +76 -0
  155. package/dist/packages/stats/synthesis.js +2 -0
  156. package/dist/packages/stats/testPassRate.d.ts +6 -0
  157. package/dist/packages/stats/testPassRate.js +1 -1
  158. package/dist/packages/stats/trend.js +1 -1
  159. package/dist/packages/stats/types.d.ts +2 -2
  160. package/dist/packages/stats/yield.d.ts +15 -6
  161. package/dist/packages/stats/yield.js +1 -1
  162. package/dist/packages/stats/yieldRows.d.ts +31 -0
  163. package/dist/packages/stats/yieldRows.js +1 -0
  164. package/llms.txt +16 -4
  165. package/package.json +3 -3
  166. package/dist/packages/stats/findingsNarrative.d.ts +0 -3
  167. package/dist/packages/stats/findingsNarrative.js +0 -1
package/AGENTS.md CHANGED
@@ -110,20 +110,20 @@ worth reporting, not a pattern to build on.
110
110
  map that is already mounted goes through `setResult()` — do not `destroy()` and
111
111
  remount.
112
112
  - **The analysis surfaces are already built — do not reimplement them.** Pass
113
- `statsSummary` to `renderWaferMap` and it mounts the Summary panel; pass
114
- `insights: { enabled: true }` and it mounts the chart suite (yield, bin pareto,
115
- boxplot, histogram, correlation, scatter, capability, and one card per
116
- `insights.sweeps` entry). `renderWaferGallery` takes the same option across a whole
113
+ `statsSummary` to `renderWaferMap` and it mounts the Summary panel. The
114
+ chart suite (Insights) is on by default (`insights: { enabled: false }` removes it): yield, bin pareto,
115
+ boxplot, histogram, correlation, scatter, capability, and a Plot tab where the reader's own plots
116
+ and sweeps (`chart: 'sweep'`, in `insights.plots`) are cards. `renderWaferGallery` takes the same option across a whole
117
117
  lot. Supply or replace the analysis later with `setStatsSummary()`. There is no
118
118
  chart-data API to hand-build them from. Charting a selection or one wafer (right-click → histogram, capability,
119
- sweeps) is built in too, with no wiring.
119
+ saved plots and sweeps) is built in too, with no wiring.
120
120
  - **A value computed from other tests is a derived test, not a host-side column.** Pass
121
121
  `derivedTests` (a `TestDef` plus an `expression`, e.g. `'abs(t[1020] - t[1010])'`) to
122
122
  `buildWaferMap`; it then behaves as a measured test everywhere, marked `†` as not
123
123
  measured. Computing it in the host and injecting it into `testValues` loses that
124
124
  mark, the missing-input rule (absent, never 0) and the collision check. A boolean
125
125
  expression must be declared `testType: 'F'`.
126
- - **A sweep's x values are data, not guesses.** Give `xValues`, or `xFromName` (a
126
+ - **A sweep's x values are data, not guesses.** In a sweep plot's `sweep.series`, give `xValues`, or `xFromName` (a
127
127
  `{x}` placeholder pattern, not a regex) when the swept value is only in the test
128
128
  name. Never derive x from test numbers: they are identifiers, not a scale.
129
129
  - **Click-to-highlight is wired, not hand-rolled.** `onSelect` reports what the user
@@ -241,7 +241,7 @@ compatibility shims for them.
241
241
  - [API reference](https://wafertools.github.io/wafermap/api/) — every type, option and return value
242
242
  - [Developer guide](https://wafertools.github.io/wafermap/guide/) — worked walkthroughs
243
243
  - [Troubleshooting](https://wafertools.github.io/wafermap/troubleshooting/)
244
- - [Examples](https://wafertools.github.io/wafermap/examples/) — 22 runnable pages, also
244
+ - [Examples](https://wafertools.github.io/wafermap/examples/) — 23 runnable pages, also
245
245
  [downloadable](https://wafertools.github.io/wafermap/wafermap-examples.zip) to run offline
246
246
 
247
247
  When a rule here and the API reference disagree, the API reference wins — tell the
package/CHANGELOG.md CHANGED
@@ -22,6 +22,379 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
25
+ ## [0.34.0] — 2026-10-06
26
+
27
+ ### Added
28
+
29
+ - **The Plot tab has Delete all plots….** It removes every saved plot, sweeps included, after a confirmation that says how many and
30
+ points to **Export plots…** for a copy; **Undo** restores the whole list for ten seconds. The button is dimmed while the list is
31
+ empty. The confirmation is a small modal that starts on Cancel, closes on Escape or ✕ as Cancel, and has no Maximize button
32
+ (`OverlayOptions.maximizable: false`).
33
+
34
+ - **The right-click Value histogram offers Edit as new plot.** It closes the histogram and opens the same test as a draft histogram plot over the same dies, in the plot editor, kept only if you add it. It appears where the host keeps plots.
35
+
36
+ - **A plotted test's limits are drawn.** In a plot of a test that has limits, the axis that measures it carries them as dashed lines — short dashes for test limits, long for spec limits, each labelled with its value. They apply to a histogram's values, a scatter's X or Y, and a box, bar or line's values when those are measured values or their mean, median, minimum or maximum (not a sum, a count, a yield or a wafer figure). The axis includes them when that leaves the data a third of it, and a limit off the edge is marked there. Customise has a **Limits** choice (`limits`: `both`, `test`, `spec` or `none`). A test whose limits the wafers disagree on has none drawn.
37
+ - **A saved sweep opens in its editor from the right-click menu**, over just the selected dies, like a saved plot: it can be edited there and the edit is saved to the sweep.
38
+
39
+ - **Yield can be plotted per ring, quadrant, bin or die position.** In a bar (pooled yield: passing dies over judged dies) or a line, Yield can be split by a per-die field, and a bar of die counts per wafer can be coloured by one. Die X and die Y can be a bar's or box's categories.
40
+ - **The plot editor says what goes with what.** In the X, Y and Colour lists a field that would leave a plot that cannot be drawn is dimmed with the reason ("Yield is per wafer: use a bar or line chart"), and does nothing when picked. A plot that cannot be drawn names the problem and what to use instead. A category given to a role that needs a number (a plot saved that way) is reported, where it used to throw. `combinationIssue` is the one rule behind both the list and the chart.
41
+
42
+ - **Right-click a bin in the legend to chart or tabulate its dies.** On a map's legend, or the gallery's lot legend (also by the Menu key or Shift+F10 on a focused entry), the drilldown menu opens on the dies in that hard or soft bin — on that wafer, or on every wafer for the lot legend — without selecting them. With several bins filtered in, right-clicking one of them opens on all of them, with **Only bin N** / **All filtered bins** to switch. Lot-stack maps are unaffected.
43
+
44
+ - **Drill down on every die a finding selects.** Right-click a finding in the Summary panel to show it and open the drilldown menu on its dies, across all the wafers of a gallery. Right-clicking a selected die on a gallery card, when dies are selected on other cards too, opens the menu on all of them, with a first row (**Only this wafer** / **All selected**) to narrow it to that card's own selection. A finding about whole wafers, with no dies to select, opens the menu on those wafers.
45
+
46
+ - **The right-click menu on a selection offers New sweep….** It opens the sweep editor as a draft on the first tests the selected dies hold, drawn over just those dies, and keeps the sweep only if the reader adds it, as New plot… does.
47
+
48
+ - **A sweep remembers its tests' names.** `series[].testNames` records what each test was called when the sweep was set up (the sweep editor and **+ New sweep** fill it in). A test whose name now differs is not drawn and the card names it, and the crossing and widths are not measured, so a sweep opened on a lot from another test program that reuses the numbers does not draw another measurement as if it were the original. A sweep without it is not checked.
49
+
50
+ - **A Data tab in Insights.** The last Insights sub-tab shows the scope as tables, one at a time: **Statistics** (the
51
+ test-values and functional-tests tables, one set per group under Group by), **Dies** (one row per die, a column per
52
+ test, sortable, drawn as a virtual table so a lot of hundreds of thousands of dies scrolls smoothly) and **Wafers**
53
+ (one row per wafer: metadata, die counts, yield, and the mean of the first 50 tests). Each has **Export CSV**,
54
+ which writes the table as shown in the order shown, and **Copy** (tab-separated, up to 200,000 cells). The Dies
55
+ export can be **Wide** (a column per test) or **Long** (a row per die per test). `InsightsOptions.defaultView`
56
+ accepts `'data'`, and the tab button carries `data-wmap-insights-tab="data"`.
57
+ - **The docked Summary panel has no CSV buttons.** The "Test values CSV" and "Functional CSV" buttons are gone from its
58
+ tables (and from the lot panel's); the **Data tables** button beside the report button opens the same tables, with Export CSV
59
+ and Copy. Insights' Data tab and the modal keep them.
60
+ - **The Summary panel's "View die list" is now "Data tables".** It opens the Data tab's tables in a modal over the wafer or lot the
61
+ panel describes — Statistics and Dies, plus Wafers for a lot — each with Export CSV and Copy, and it works with Insights
62
+ turned off. The Dies table is virtual, so `dieList.maxRows` no longer limits it (it still limits the coordinate-less map
63
+ replacement and the "+N dies without position" footer). `dieList.enabled` gates the button as before.
64
+ - **A die row in a table shows that die on the map.** In the Dies table (the Data tab, a drilldown selection, the Summary panel's
65
+ Data tables), clicking a row steps the table aside (or leaves Insights) and rings that die on its map, on its own card in a
66
+ gallery, scrolled into view. The ring is not a selection: nothing else fades, and it goes with the next click on the map or Esc.
67
+ Rows are clickable only where the host has a map to show.
68
+ - **A gallery can pick whole wafers.** Ctrl/Cmd+click a card's header to add or remove its wafer (outlined, counted in the
69
+ toolbar, cleared by clicking the count). Right-clicking a picked card opens the drilldown menu on every die of every picked
70
+ wafer, with a Wafers table; dies selected on a map take precedence.
71
+ - **A gallery can select the same dies on every wafer.** A new toolbar toggle, **Select on every wafer** (off by default),
72
+ applies a box, a click or a clear made on any card at the same die positions on every card, so a region (the edge ring, a
73
+ scratch zone, a reticle corner) is read lot-wide. Right-click, the Menu key or the Chart button on any card or its header
74
+ then opens the drilldown menu on those dies across all the wafers: the charts, plus Dies, Test statistics and Wafers
75
+ ("selected at the same die positions on N wafers"), saved under the gallery's own file names. There is no option for it:
76
+ it is a toolbar toggle only. The button carries a count of the selected die positions; clicking it with a selection
77
+ showing clears the selection and stays on, and turning it off clears the selection on every card.
78
+ - **The Insights scatter is interactive.** Hovering a point names its wafer and die and its two values; clicking it opens that wafer
79
+ on the X test (a single-wafer host shows the test on its map); dragging a rectangle selects the dies inside it, across any
80
+ number of wafers, ringed until the X/Y test changes or empty space is clicked, and opens the drilldown menu for them —
81
+ the histogram, capability and sweep charts, and the Dies, Test statistics and, for more than one wafer, Wafers tables.
82
+ Scatter points now carry their `die` and `waferIndex` (`ScatterPoint`, `ScatterItem`).
83
+ - **Drilldown can open the selection as a table.** The right-click menu on a map selection, a wafer or a chart mark gains
84
+ **Dies** and **Test statistics**: the Data tab's tables over just those dies, in a modal that names the population,
85
+ with Export CSV and Copy (a saved file is named for the selection and carries a Wafer column). On a bins-only
86
+ map the charts and Test statistics are greyed with the reason.
87
+
88
+ - **A Plot tab in Insights: build, keep and reload your own plots.** The last Insights sub-tab is a chart builder. **+ New
89
+ plot** opens a plot beside a large copy of its chart; choose the chart type (scatter, histogram, box, bar or line), the
90
+ field for X, Y and colour (any parametric test, a die's X or Y, a wafer's yield or die count, ring, quadrant, hard or soft bin,
91
+ site, or a wafer or lot field such as split, slot or temperature), and each axis's title, limits, log scale and
92
+ direction. The field lists are grouped and filter by name or test number. Titles are automatic until typed in.
93
+ Colour follows the Group by control unless the plot names its own field, and a scatter can be coloured on a gradient by
94
+ a measured value or a wafer figure. A bar of Yield is pooled (passing dies over judged dies) rather than an average of
95
+ percentages, a plot can draw one mark per wafer with its dies' values combined by mean, median, minimum, maximum, sum
96
+ or count, and a per-wafer figure is never split by a per-die field (it is reported instead). Every plot states its wafers and dies, how
97
+ values were combined, and how many dies were left out for a missing value. Points hover, click to open their wafer,
98
+ and drag to select dies into the right-click menu. A plot is a recipe with no population, so it is kept for the next
99
+ lot: `insights.plots` supplies the list, `insights.onPlotsChange` reports it after every change (a burst of typing is
100
+ one call), and **Export plots…** / **Import plots…** carry it as a `wafermap-plots` file (`onPickPlotsFile` for a
101
+ native dialog). A plot that needs a test the open lot lacks is kept and greyed with the reason, and a test is matched
102
+ by number and checked by name, so a plot saved against another test program is reported rather than drawn against the
103
+ wrong measurement. The Plot tab is loaded the first time it is opened.
104
+ - **Add examples, and titles that say when they are out of date.** The Plot tab's **Add examples** draws one plot of each
105
+ chart type the lot can show (scatter of the first two tests, histogram, box by wafer, bar of yield by the first lot field
106
+ that divides the wafers, line over wafer order, and a sweep of the first tests in test order) and adds only those not
107
+ already there. A title the reader typed is checked
108
+ against its plot: the card and the editor say when it names a test the plot no longer shows, or names one of its two
109
+ fields but not the other, and offer the automatic title. Copying a plot numbers its copies ("Vth (copy 2)") instead of
110
+ stacking marks, and a copy of an untitled plot stays untitled so its title keeps following its fields.
111
+ - **Clicks on Insights charts open what the mark counts.** A plot's point, bar or box opens its wafer on the plot's own test
112
+ (it opened the bin map before), or shows the test on the map in a single-wafer host. The bin pareto (and the grouped
113
+ one's sub-bars), a pass-rate row (the dies failing that test, judged as the card is), a bar of the value histogram, a
114
+ ring or quadrant of the yield diagrams, and a plot's histogram bar, non-wafer bar or box, and line point each open the
115
+ right-click menu on exactly the dies they count; a Wafers-table row opens its wafer. `dieFailsTest`, `diesInBucket`,
116
+ `diesInRegion` and `bucketIndexOf` are the rules those picks share with the tallies they match.
117
+ - **More of Insights is clickable, and a chart with many groups keeps its colours apart.** With Group by on, a column of the
118
+ overlaid value histogram opens the menu on that bucket's dies (across the groups, or the one the legend has emphasised);
119
+ a level of a sweep curve opens the dies measured there; a drag across the wafer-to-wafer trend selects the wafers it
120
+ crosses, ringed, and opens the menu on their dies (a click on a point still opens that wafer). The categorical series
121
+ palette has sixteen colours instead of eight (the first eight are unchanged): the second round was chosen for distance
122
+ from the first in CIELAB and for staying distinct under deuteranopia and protanopia, so a lot of 13 wafers coloured by
123
+ wafer no longer gives two wafers the same colour. `categorical(i)` wraps past sixteen. `wirePointInteractions` gains
124
+ `locate`, for charts whose marks are columns.
125
+ - **Saved plots in the right-click menu.** The drilldown menu has a **Plots** section: each saved plot is drawn over the
126
+ selected dies, and **New plot…** starts a draft on the selection that is kept only if it is added.
127
+ - **`readPlotsFile` and `writePlotsFile`** (and the `PlotSpec` type) read and write the plots file. The reader is
128
+ lenient (it keeps every readable plot and names each dropped setting), keeps settings a newer version wrote, and
129
+ keeps a plot whose chart type it does not know.
130
+ - **Sweeps are plots, with an editor.** A sweep is a plot with `chart: 'sweep'`: a card on the Plot tab beside the reader's
131
+ other plots, with Edit, Duplicate and Delete (and Undo), kept, exported and imported like any plot, and a row in the
132
+ right-click menu's Plots section over the selected dies. A level of its curve opens the dies measured there, as before.
133
+ **+ New sweep** starts one on the lot's first tests, and **Edit** opens a sweep editor beside a live copy of the curve:
134
+ the title, a block per series (its tests in sweep order, typed as numbers and ranges such as `1010, 1011, 1020..1030`,
135
+ and whether X is the test order, typed values or read from the test names), the axis title, unit and scale, whether the
136
+ first two series' crossing is marked, and the Y levels at which their width is measured. Text that is not a test number
137
+ or a range is flagged where it is typed and not applied. A sweep whose tests are not in the open lot is kept and greyed,
138
+ and draws again when a lot with its tests is opened. `PlotSpec` gains the optional `sweep` (a `SweepSpec` without its
139
+ `id` and `title`), and `readPlotsFile` also reads a `tsmap-sweeps` file, turning each sweep into a sweep plot under
140
+ the same `id`. When a name pattern (`xFromName`) finds no X in a test's name, the warning on the chart shows the first
141
+ three of those names, as they are in the data, so the pattern can be written against them. The sweep editor shows the same: under a name pattern it lists the first four
142
+ test names and what the pattern reads from each as it is typed, with a folded list of worked **Pattern examples**
143
+ (`@ {x}` on `Fmax @ 0.55 V`, `-{x}` on `1234-5`), which a test keeps true against the matcher. A name pattern's `?` matches exactly one character
144
+ (`123?{x}` reads 4 from `12314`), alongside `*` for any run of text.
145
+ - **A plot's JSON uses `chart` and `fields`.** A plot names its kind of chart in `chart` (`scatter`, `histogram`, `box`, `bar`,
146
+ `line` or `sweep`) and the field for each role in `fields` (`x`, `y`, `color`); a sweep has no `fields`.
147
+ - **Reset and Cancel in the plot and sweep editors.** Edits are kept as they are made, so the editor's footer, which stays in
148
+ view while the settings scroll, has **Reset** (back to the plot as it was when the window opened; disabled until something
149
+ changed) and **Cancel** (the same, then close; a draft is discarded). Closing with the window's own button keeps the changes.
150
+
151
+ ### Breaking
152
+
153
+ - **Pass bins have one default, applied once.** Bin 1 passes when an input states no pass bins, and `buildWaferMap` applies that
154
+ default and nothing else does. Every view, report, chart and table takes the pass bins of the map it describes and refuses to
155
+ state a yield without them: `renderSummaryReport`'s `passBins`, `buildView`'s `passBins` option and each item of a lot report are
156
+ required unless the item is a built map, which carries its own. A chart or table opened on a selection, a wafer, a gallery card or
157
+ a saved plot judges by that wafer's own pass bins, so a lot built with pass bins 3 and 5 reports the same yield everywhere
158
+ it is shown. The gallery's stacked modes judge by the pass bins and ring count the wafers share and are not offered for wafers
159
+ that differ. The ring count follows the same rule: `buildWaferMap` applies its default (4) and every ring, quadrant and ring
160
+ figure reads the map's own, so `renderSummaryReport`'s `ringCount` and `buildView`'s `ringCount` option (when rings are drawn)
161
+ are required unless the map is a built one.
162
+
163
+ - **The Maps | Insights switch is at the far right of the toolbar row, and the toolbar has no Insights button.** The switch sits after the toolbar and stays in the same place in both views (alone at the right edge when there is no toolbar), so it is both the way into Insights and the way back. The toolbar's Insights button and its `data-wmap-insights-btn` hook are gone: a host that clicked the hook should click `[data-wmap-view="insights"]` (or `[data-wmap-view="maps"]`), whose `aria-selected` says which view is showing. `setInsightsOpen` and `insights.enabled` are unchanged.
164
+
165
+ - **Right-click on a map, a gallery card or a chart mark always opens the drilldown menu.** The menu always has **Dies**, and
166
+ lists charts and plots where there is something to draw (a parametric test or a saved plot), so a host's own context menu
167
+ on those surfaces does not appear. The toolbar's Chart button is always shown and is labelled "Charts and tables for …".
168
+ - **`onSaveText` can receive a `Blob`.** `SaveTextHandler`'s first argument is now `string | Blob`. A table of a million
169
+ cells or more (a lot's die list, 400k dies × 50 tests being 20 million) is written in slices and handed over as a
170
+ `Blob`, so no single string holds the whole file. Smaller tables are still a string, saved before the call returns.
171
+ A host that writes the text to a file should write `blob.stream()` for a `Blob`; one that reads it uses
172
+ `await blob.text()`.
173
+ - **Insights is on by default.** `insights.enabled` now defaults to `true` wherever there is a toolbar: always
174
+ for `renderWaferGallery`, and for `renderWaferMap` unless `showToolbar` is `false` (a chromeless map stays a
175
+ plain map). Hosts that do not want it pass `insights: { enabled: false }`. Gallery cards are never Insights
176
+ hosts. The chart code still downloads only when Insights is first opened.
177
+
178
+ ### Changed
179
+
180
+ - **Sweeps are drawn on the Plot tab; the Sweeps tab is gone.** Each sweep is a card there, in the same list as the other
181
+ plots. `insights.sweeps` and `insights.onRemoveSweeps` are deprecated and still work: each entry is drawn as a sweep plot
182
+ under its own `id` (a plot already saved under that `id` takes precedence, so a host that keeps both shows it once), the
183
+ first use logs a notice, and deleting one on the Plot tab calls `onRemoveSweeps` so the host stops supplying it.
184
+ `defaultView: 'sweeps'` opens the Plot tab. The right-click menu lists sweeps with the plots, in its Plots section.
185
+
186
+ - **A chart saved as a PNG says what it is, and a printed chart says the same.** Every Insights chart card now reads back
187
+ what it shows around its canvas: its title, what its controls are set to (`Test: Vth`, `Wafer: W03`, `X: Vth · Y: Idsat`,
188
+ ticked toggles), the lines it states about itself (population and N, `box = Q1–Q3, …`, the normalisation of the capability
189
+ chart, the correlation sample note), and its colour key. A saved image puts these in a header above the chart, with the
190
+ instructions to click or drag removed; the camera button's file is named for the title as it reads then. A chart whose key
191
+ is a gradient (a coloured scatter, the correlation matrix) gets a colour bar with its range. The image takes the card's own
192
+ background instead of white, so a dark theme saves a legible dark chart. In print, the controls and on-screen hints are
193
+ hidden (`data-wmap-noprint`, one `@media print` rule) and each card shows a block, refilled just before printing, giving the
194
+ same settings and lines. Panels mark the text that belongs in them with `data-wmap-caption`. `makeWaferSelect`'s accessible
195
+ name is now "Wafer" (it was its "All wafers" label). `saveCanvasPng` gains an optional header and is otherwise unchanged.
196
+ - **Controls that act on the screen are left off a printed page**: the camera and expand buttons on chart cards, and the Plot
197
+ tab's New plot, Add examples, Import, Export, Edit, Duplicate and Delete with their notices.
198
+ - **The Overview no longer carries the test-values and functional-tests tables.** They moved, unchanged, to the Data
199
+ tab's Statistics view; the Overview ends with a link to it. Hosts that read the "Test values CSV" or "Functional
200
+ CSV" button from the Overview should open the Data tab first. The Summary panel's own tables are unchanged.
201
+ - **Large CSV exports stay responsive and use less memory.** A big table is built in slices of about 30 ms, with
202
+ progress on the die list's Export CSV button, instead of freezing the page. At 400k dies × 50 tests (201 MB) the
203
+ export completes within 250 MB of heap where a single string needs about 400 MB, and it is no longer limited by the
204
+ engine's maximum string length.
205
+ - **CSV exports carry full-precision numbers.** The test-values, functional-test, die-list and correlation exports write
206
+ every measured value and statistic as a plain number (`452.123456789`, `1.5e-7`) instead of the screen's four-figure
207
+ formatting (`452.1`, `150E-9`). A value from a single-precision tester field is written as the shortest decimal that
208
+ is still that value (`0.5123457`, not `0.51234567165374756`); anything else keeps up to 15 significant digits. Percentages keep one decimal, Ppk three, and
209
+ correlation r six. A non-finite value is an empty cell. All four exports are written by one function
210
+ (`core/tableCsv.ts`), so quoting and number format are the same in each.
211
+ - **The metadata fields strip fills the width it has.** On the gallery legend and the Insights strip, fields are taken in
212
+ priority order and each lists as many of its values as fit (up to 12), instead of three; a field that cannot fit even
213
+ one value, and those after it, move behind `N more fields`, so the button appears only when something is hidden. The
214
+ strip re-fits when its width changes. The Data tab's Wide | Long choice is labelled **Export format**.
215
+ - **The identity strip has one overflow control.** A field with many distinct values shows its count in the
216
+ label and an ellipsis (`Lot (5): LOT-A, LOT-B, LOT-C, …`); the only button is `N more fields`, which
217
+ reveals the remaining metadata fields.
218
+ - **A Map | Insights switch leads the chrome row.** With `insights.enabled`, the first control in the row
219
+ above the map (single map: Map | Insights; gallery: Maps | Insights) is a labelled two-tab switch that stays
220
+ in the same place in both views, so it works as a toggle and makes Insights easy to find. It replaces the
221
+ back tab at the start of the Insights tab row, which now holds only Overview · Distributions · Correlation.
222
+ The toolbar's Insights button is unchanged. Hosts waiting for the Insights chunk should poll
223
+ `button[data-wmap-insights-tab]`, not `button[role="tab"]`.
224
+ - **A labelled "Summary" tab opens the Summary panel while it is closed.** A slim vertical tab on the outer
225
+ edge of the map area (single map and gallery, left or right placement) shows only while the panel is closed
226
+ and a Summary exists, tints like the toolbar icon when there are notable findings, and opens the panel. The
227
+ toolbar's Summary button is unchanged.
228
+ - **"What stands out" gives the top item the emphasis.** The largest item keeps its tinted box and full
229
+ sentence; the others are one compact list (impact marker, the item in a few words, share of the dies),
230
+ in the panel and the report alike. The summary's `also` line is replaced by that list.
231
+ - **One σ rule: the sample standard deviation (n−1).** The Test Values table, per-test statistics, the
232
+ lot trend, the lot drift σ and the report's pooled σ all divide by n−1, as the capability indices
233
+ already did, so a cell shows the same σ whichever path filled it. A displayed σ is √(n/(n−1)) times
234
+ the population σ of the same values (about 2.5% at n = 20; negligible on large lots). Exported
235
+ `sampleVariance` in `stats/math.ts` is the single implementation.
236
+
237
+ ## [0.33.0] — 2026-10-02
238
+
239
+ ### Breaking
240
+
241
+ - **`sectorCount` is 4, 8 or 16.** The compass names cover 16 bearings, so 32 has no names to give its sectors;
242
+ it is corrected to the default with an `'analysis-option-corrected'` warning, like any other unusable value.
243
+
244
+ ### Changed
245
+
246
+ - **An expanded map's window widens for the Summary panel.** Opening the panel in a map's Expand modal, or in a wafer
247
+ opened from a gallery, makes the window wider by the room the panel needs, so the map keeps its size instead of
248
+ giving up more than half its width; closing the panel narrows it again. The window stays within the screen (a
249
+ floating window moves left if it would run off the edge), does nothing when maximised, and leaves a width the user
250
+ has dragged it to. A wafer opened in a separate browser window is resized the same way where the browser allows it.
251
+ - **The report builders load when a report is opened.** The Summary panel's report button and the gallery's load
252
+ `renderSummaryReport` with a dynamic `import()` instead of with the map, which takes about 10 KB gzipped out of what
253
+ rendering a map downloads (core is ~143 KB with this release's additions); the first report opens a moment later while it loads.
254
+ `renderWaferReportHtml` and `renderLotReportHtml` are unchanged public exports.
255
+ - **The Summary panel leads into the report, and the report acts on the map.** "What stands out" ends with a **Full
256
+ report** link that opens the same report the Summary report button does, which begins with that same section. In
257
+ a report opened from the panel, a click on a finding's row closes it and shows that finding on the map, as its
258
+ row in the panel does (on a lot report that is split into several groups, a finding the panel does not hold is
259
+ ignored). Opened on its own, the report is a plain page and its rows do nothing.
260
+ - **The reports and the Summary panel count and order bins, findings and statistics the same way.** One count of dies
261
+ per bin (partial and edge-excluded dies left out) and one order (pass bins first, then fail bins by count) feed the
262
+ panel's bars and the reports' tables, under one title that states the population ("Hard Bin Breakdown — % of dies
263
+ (N=…)"); the reports show the soft-bin breakdown as well when the data has soft bins, in the same columns for a
264
+ wafer and a lot. The reports' findings table is arranged as the panel's list is: each spatial pattern with the
265
+ findings it explains beneath it, then the rest by region, most severe first. The wafer report's Test Values table
266
+ has the panel's full columns (N, min, quartiles, median, mean, max, σ) and the share of dies inside each test's
267
+ limits; a lot's has N, min, mean, max, σ (pooled exactly) and limit yield, and says why it has no quartiles. The
268
+ lot overview names the unweighted mean of wafer yields "Mean per-wafer yield" everywhere.
269
+ - **The Summary panel and the HTML reports read yields the same way.** One computation gives each wafer's
270
+ difference from the lot median (with the lot's outlier rule naming the outliers) and each ring or quadrant's
271
+ difference from the wafer or lot it divides. The panel's wafer and region rows print that difference in points
272
+ and tint a shortfall of 1, 2 or 4 or more points, as the report's tables do; the report's wafer table says
273
+ "low outlier" in words, as the panel always has. The panel's findings mark severity as three dots filled by level
274
+ rather than a colour-only dot, as the reports do. The reports draw bin bars in the colour the map gives that bin
275
+ (the live colours when opened from a panel, the default palette otherwise).
276
+ - **The Summary panel opens with "What stands out"**, the same synthesis the HTML reports open with (a headline, up to
277
+ three items ranked by the dies each costs, an "also" line, and what was compared), in place of the findings
278
+ narrative above the findings list. It is a section of its own, so a clean wafer or lot says "Nothing stands out"
279
+ where the findings list is absent. The names in it (a region, a bin) select that finding on the map; the Findings
280
+ section's Detail button opens the same sentences above the full list. Where the wafers in a lot were judged by
281
+ different pass bins, the items rest on yield alone and name no pass bins.
282
+ - **The weakest finding severity is called "Minor" where it is shown** (the Summary panel's severity filter and the
283
+ reports); the value in `StatsFinding.severity` is unchanged.
284
+ - **A merged region on a wafer is graded with its constituents' multiple-testing correction.** The merged finding's
285
+ p-value is multiplied by the weakest constituent's adjusted-to-raw ratio before severity is assigned, the same rule the
286
+ lot merge applies, and `stats.adjustedPValue` is set. A merge can no longer grade more severe than the correction its
287
+ parts carry allows, so a few merged findings show a lower severity.
288
+ - **A lot stack by standard deviation or count carries no test limits.** Its values are spreads and tallies, not
289
+ measurements of the test, so `result.testDefs` has the limits removed and nothing is judged against them: no
290
+ limit fail findings, limit yield, capability index or out-of-spec colouring. Stacks by mean, median, minimum
291
+ and maximum keep them.
292
+ - **A sector run and a quadrant over the same part of the wafer are one finding.** "Sectors W–S" and
293
+ "Quadrant SW" make one statement, so the one with the smaller p-value is shown and the other is listed
294
+ under it (`absorbedIds`). They must be the same comparison (metric, variable and direction) and cover
295
+ angles that overlap by at least three fifths of their union, by geometry. A single sector inside a
296
+ quadrant, and a mean against a limit fail rate, stay separate. Wafer and lot findings follow the same rule.
297
+ - **Lot findings merge adjacent regions.** A run of adjacent sectors, quadrants or rings that carries one
298
+ signal is one lot finding ("Sectors E–N", "Rings 1–2"), as on a single wafer. Each wafer's regions are
299
+ added up, the rest being the wafer's other regions, and those per-wafer figures are combined across the
300
+ wafers as the single regions are (Stouffer's Z, the same gates). The run is adjusted with the
301
+ Benjamini–Hochberg multiplier of its weakest region and is reported only if it passes on its own;
302
+ otherwise its separate rows stay. "N/M wafers" counts the wafers where the merged region itself differs at
303
+ the analysis's significance level.
304
+ - **A merged run is compared with the rest of its region family.** As for each region on its own: sectors
305
+ leave out the centre dies, so a merged sector run's "rest of the map" is the other sectors' dies.
306
+ - **Adjacent regions merge per metric.** A test's mean finding and its limit fail rate are kept apart when
307
+ adjacent sectors, quadrants and rings are merged, so a run such as sectors E, NE and N reads as one
308
+ finding ("Sectors E–N"), and a quadrant with both a mean and a limit finding lists both. A run of limit
309
+ fail rates merges too, recomputed over the union of its regions. A run of quadrants is named in order
310
+ round the wafer.
311
+ - **A lot's spatial pattern is decided from the lot.** `analyzeWaferLot` stacks the lot's wafers by die
312
+ position, finds the positions where failures recur (an exact binomial test on each position's 3×3
313
+ neighbourhood, Benjamini–Hochberg across positions), classifies that area with the same classifier and
314
+ thresholds as a single wafer, and reports the pattern once, on every wafer whose failures are concentrated
315
+ in it. A pattern visible on every wafer is reported on every wafer under one label, with the ring, edge-arc
316
+ and cluster findings it explains listed under it. Wafers on a different grid, and lots too small for
317
+ failures to recur significantly, keep the per-wafer count. The wafer-level classifier and its thresholds
318
+ are unchanged (WM-811K: 64.1% overall).
319
+ - **Outlier wafers have one rule**, read by both the lot findings and the Summary panel's
320
+ Wafer Yield list, so they always name the same wafers. From 3 to 7 wafers it is Dixon's Q
321
+ test on the lowest and highest wafer (95% → `notable`, 99% → `unusual`); from 8 wafers,
322
+ Tukey's fences over per-wafer yield (1.5 × IQR → `notable`, 3 × IQR → `unusual`). Either
323
+ way the wafer must also be at least 3 yield points from the median. The
324
+ finding's `stats.method` is `'dixon-q'` or `'tukey-fence'`, and `effect.effectSize` is
325
+ that test's statistic (Q, or the distance from the median in IQR units). The Wafer Yield
326
+ list labels high outliers as well as low ones.
327
+
328
+ ### Added
329
+
330
+ - **Compact layout for multi-project wafers.** A **Compact layout** row in the Overlays menu, on a single map and on the
331
+ gallery, draws the dies on a grid with the empty rows and columns removed and each group of dies outlined, so a
332
+ sparse layout fills the map instead of leaving each die a few pixels wide. It is offered when the occupied columns
333
+ and rows repeat at a regular pitch, or at the `reticleConfig` width and height, or a multiple of it (a product on
334
+ every second reticle), when every wafer supplies the same ones; `viewOptions: { compact: true }` applies it regardless. Only the layout changes:
335
+ every die is still drawn and counted, so legends, yield and statistics are the same, and hover text and axis
336
+ labels give original die coordinates. A die with no bin keeps its place. The notch marker follows rotation and
337
+ flips, and so does the XY indicator, which sits in a margin beside the grid. A gallery builds one layout from every
338
+ wafer shown, so its cards stay comparable. The wafer outline, ring, quadrant and reticle overlays describe the
339
+ physical wafer and are not drawn in this layout.
340
+ - **Rotating or flipping a map switches the XY indicator on.** The first Rotate or Flip from the Orientation menu, on a
341
+ single map or a gallery, turns on the +X/+Y arrows in whatever layout is showing, so the reader can see which way
342
+ the die coordinates run. They stay on until switched off in the Overlays menu, and **Reset orientation** does not
343
+ change them.
344
+ - **An Axis labels row in the Overlays menu.** The die-coordinate labels along the bottom and left edges appear when the
345
+ map is zoomed, as before; the row shows them always, or hides them, and **Clear overlays** returns it to that default.
346
+ `viewOptions: { showAxes }` is the same choice for a host. In the compact layout the labels name the first column
347
+ or row of each group of dies, which are the reticle boundaries, and every die once the cells are wide enough to
348
+ label them all.
349
+ - **Gallery cards fit the compact layout.** With the compact layout on, a card is as tall as its map needs and as wide
350
+ as that layout calls for, rather than sized for the wafer it replaces, so more cards fit on screen. A quarter turn
351
+ swaps the card's width and height. Turning the layout off restores the square card.
352
+ - **A multi-project wafer example and guide section.** The new Multi-project wafers example page shows the compact
353
+ layout on a synthetic wafer where two products share a reticle, and the guide's "Compact layout for multi-project
354
+ wafers" section covers the option, when it is offered, galleries, axis labels and the diagnostics.
355
+ - **Layout diagnostics.** A **Layout diagnostics** row beside it shows what the compact layout's detector saw: counts,
356
+ periods and scores only, with no die positions, bins, test values or wafer identity. **Copy** and **Save as file**
357
+ let someone report why a layout was or was not recognised without sharing their data.
358
+ - **A lot's yield or a test's mean can be reported as drifting across the wafers.** `analyzeWaferLot` runs a
359
+ Mann–Kendall trend test (with the Theil–Sen slope to size it) on the per-wafer yield, and on each test's per-wafer
360
+ mean when per-test statistics were computed, for lots of five or more wafers. The p-values are corrected together
361
+ (Benjamini–Hochberg), and a trend must also move far enough to matter: two points of yield, or half a within-wafer
362
+ σ of a test. A finding has id `drift:yield` or `drift:test:<number>`, level `'inter-wafer'`, family `'wafer'`,
363
+ method `'mann-kendall'`, and its summary says "(input order)": the wafers' order is a physical one only when the host
364
+ says so. The reports and the Summary panel list the strongest as a **Watch** line under "What stands out", with up to
365
+ one more for a test whose Ppk against its limits is under 1.0 and that costs no dies of its own. Watch is kept apart
366
+ from the items, which are ranked by dies lost.
367
+ - **Both HTML reports open with a "What stands out" section.** A headline (the yield, and for a lot its
368
+ spread across wafers), up to three items ranked by the dies each costs, and a line saying what was
369
+ compared. An item is a region whose pass rate fell, a fail bin that rose, a wafer well below the lot, a
370
+ parametric test outside its limits on a share of the dies, or a functional test that fails;
371
+ findings standing on the same dies are one item, a repeated spatial pattern leads its item, and the fail
372
+ bins that make up a region's shortfall are named with their share. An item's impact (High, Moderate or Low) is the
373
+ higher of its share of the lot's dies (4% and 2%) and its share of the dies that fail (40% and 15%), so a small area that
374
+ is most of a good lot's loss reads as high impact. Only an item costing at least one
375
+ yield point of the analysed dies is listed; otherwise the section says nothing stands out. Each figure links
376
+ to its row in the Findings table, which now carries an anchor per row (a test item names the test and
377
+ has no row to link to).
378
+ - **Both HTML reports have a new layout.** Severity is three dots and a word (Unusual, Notable, Minor) in place of
379
+ the coloured badge, so it reads in a black-and-white print and for colour-blind readers; findings rows and the
380
+ items in "What stands out" carry a light tint and a left edge. Yields, regions, wafers and bins are drawn with a
381
+ bar behind the figure, and a wafer or region is tinted only when it is 1, 2 or 4 or more points below its
382
+ reference, so a lot with nothing wrong has no tint. The key figures are tiles, pass bins are written in grey, and
383
+ a contents line leads the page (not printed). Items over the three shown go on one "Also over a yield point" line.
384
+
385
+ ### Fixed
386
+
387
+ - An outlier wafer's finding reports its yield difference as a fraction in `effect.absoluteDelta`, like every
388
+ other yield finding, so the findings table shows "−4.3 pp" for a wafer 4.3 points below the lot median.
389
+ - The spatial-pattern classifier calls a failing region a scratch only when it reaches beyond
390
+ the central zone (35% of the radius). An elongated cluster wholly inside it is a centre
391
+ cluster. On the WM-811K benchmark (25,519 wafers): centre recall 59.9% → 60.5%, scratch
392
+ precision 33.3% → 35.7%, scratch recall 24.3% → 24.1%, overall 64.0% → 64.1%.
393
+ - A lot's repeated spatial pattern lists, as its `relatedIds`, the repeated findings that
394
+ each wafer's own pattern claimed on every wafer they appear on — so an edge arc that is part
395
+ of the edge ring on each of its wafers is shown under the lot's edge pattern, as at wafer
396
+ level, not as a separate lot finding.
397
+
25
398
  ## [0.32.0] — 2026-09-27
26
399
 
27
400
  ### Breaking
@@ -1543,7 +1916,7 @@ export's notice names its replacement; the old names are still removed in 0.31.0
1543
1916
  placements take a fixed 180px band with `overflowY: hidden`, which most wafers' content
1544
1917
  exceeds, so the last visible row was cut mid-line with nothing indicating more existed. All
1545
1918
  placements now scroll, the way `'right'`/`'left'` already did. (Whether this content suits a
1546
- wide-short band at all is a separate open question — see TODO.md.)
1919
+ wide-short band at all is a separate open question.)
1547
1920
  - **Closing and reopening Insights no longer discards the shared axis toggles.** `axisPrefs`
1548
1921
  ("axis includes limits" / "clip outliers") lived inside `renderDistributionsSection`, which
1549
1922
  `render()` rebuilds — so a scope change, or simply toggling Insights off and on, silently
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-~50%20kB-blue)
8
+ ![bundle](https://img.shields.io/badge/data%20layer%20min%2Bgz-~64%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 ~50 kB
16
+ Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~64 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
 
@@ -31,9 +31,9 @@ wafermap renders interactive wafer maps from semiconductor prober output. Hard b
31
31
  - Geometry inference — pass full physical dimensions or raw prober step positions; die pitch, wafer diameter, and coordinate origin are resolved automatically
32
32
  - `renderWaferMap` — interactive canvas map with toolbar, zoom/pan, tooltips, die selection, and summary panel
33
33
  - `renderWaferGallery` — lot-level card grid with shared controls and click-to-expand
34
- - Insights tab (`insights: { enabled: true }`) — an in-toolbar chart suite (yield, per-test pass rate, bin pareto, capability, boxplot, histogram, wafer-to-wafer trend, correlation, scatter) computed from the same wafer/lot data, for one wafer or the whole gallery
34
+ - Insights tab (`insights: { enabled: true }`) — a chart suite behind the Maps | Insights switch (yield, per-test pass rate, bin pareto, capability, boxplot, histogram, wafer-to-wafer trend, correlation, scatter) computed from the same wafer/lot data, for one wafer or the whole gallery
35
35
  - Derived tests (`derivedTests`) — tests computed per die from other tests (`abs(t[1020] - t[1010])`), which then work everywhere a measured test does, marked † as not measured
36
- - Parametric sweeps (`insights.sweeps`) — an ordered run of tests read as a response curve, with the crossing and widths between two curves measured; x values given directly or read from the test names, on a linear or log axis
36
+ - Parametric sweeps (plots with `chart: 'sweep'`, edited on the Plot tab) — an ordered run of tests read as a response curve, with the crossing and widths between two curves measured; x values given directly or read from the test names, on a linear or log axis
37
37
  - Drilldown — right-click selected dies, a wafer card or a wafer's bar to chart just that population, with the population stated on the chart
38
38
  - `analyzeWaferMap` / `analyzeWaferLot` — spatial analysis across rings, quadrants, sectors, and reticle positions; failure cluster detection; lot trend series
39
39
  - Pure ES modules, no server, no runtime dependencies — works in React, Svelte, Vue, plain HTML, or a Web Worker
@@ -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 **16** of its ~100 exports. Read the
78
+ application built on this library, imports **18** 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,12 +1,19 @@
1
1
  import type { Die } from '../core/dies.js';
2
- import { type TestDef } from '../renderer/buildWaferMap.js';
3
- import type { SweepSpec } from '../stats/sweep.js';
2
+ import type { Wafer } from '../core/wafer.js';
3
+ import type { TestDef } from '../renderer/buildWaferMap.js';
4
4
  /** One wafer's share of a population. */
5
5
  export interface DrilldownItem {
6
6
  /** The wafer's display label. */
7
7
  label: string;
8
8
  dies: Die[];
9
9
  waferIndex?: number;
10
+ /** The wafer's geometry and metadata, when the gesture knew it — a table of these dies
11
+ * uses it for ring/quadrant and for the lot and product columns. */
12
+ wafer?: Wafer;
13
+ /** This wafer's own pass bins (the result's, never a default): every yield a chart or table of these dies states judges by them. */
14
+ passBins: readonly number[];
15
+ /** This wafer's own ring count (the result's, never a default): a ring or quadrant of these dies is the map's ring. */
16
+ ringCount: number;
10
17
  }
11
18
  /** A population a chart can be opened on. */
12
19
  export interface DrilldownSource {
@@ -23,14 +30,7 @@ export interface DrilldownSource {
23
30
  * aggregates): every chart is then unavailable, with this as the reason. */
24
31
  notMeasuredReason?: string;
25
32
  }
26
- /**
27
- * Whether any drilldown chart could exist for this data — decides whether a
28
- * right-click is taken over at all, without loading the drilldown chunk. The
29
- * distribution charts need a parametric test, a sweep needs to be defined;
30
- * with neither (a bins-only map) right-click stays the browser's or host's.
31
- * `targetsFor` in drilldown.ts is the list this summarises.
32
- */
33
- export declare function hasDrilldownTargets(testDefs: readonly TestDef[] | undefined, sweeps: readonly SweepSpec[] | undefined): boolean;
33
+ export declare const LOT_STACK_REASON = "This map stacks a lot: its dies are per-position aggregates, not measured dies";
34
34
  interface WaferFacts {
35
35
  /** The wafer's real identity, if it has one — never a positional stand-in,
36
36
  * which would read as an ID ("Wafer 3 (no ID)") in a chart title. */
@@ -39,6 +39,11 @@ interface WaferFacts {
39
39
  isLotStack?: boolean;
40
40
  activeTest?: number;
41
41
  waferIndex?: number;
42
+ wafer?: Wafer;
43
+ /** The wafer's pass bins, from its built result. */
44
+ passBins: readonly number[];
45
+ /** The wafer's ring count, from its built result. */
46
+ ringCount: number;
42
47
  }
43
48
  /** Dies the user selected on one wafer's map. */
44
49
  export declare function selectionPopulation(selected: Die[], f: WaferFacts): DrilldownSource;
@@ -1 +1 @@
1
- import{isParametricTest as s}from"../renderer/buildWaferMap.js";export function hasDrilldownTargets(t,e){return(e?.length??0)>0||(t??[]).some(s)}const n="This map stacks a lot: its dies are per-position aggregates, not measured dies";function r(t,e,a){return{items:[{label:a.waferLabel??"this wafer",dies:t,waferIndex:a.waferIndex}],population:e,testDefs:a.testDefs,activeTest:a.activeTest,notMeasuredReason:a.isLotStack?n:void 0}}export function selectionPopulation(t,e){return r(t,e.waferLabel?`selected on ${e.waferLabel}`:"selected",e)}export function waferPopulation(t,e){return r(t,e.waferLabel?`on ${e.waferLabel}`:"on this wafer",e)}
1
+ export const LOT_STACK_REASON="This map stacks a lot: its dies are per-position aggregates, not measured dies";function s(a,e,t){return{items:[{label:t.waferLabel??"this wafer",dies:a,waferIndex:t.waferIndex,wafer:t.wafer,passBins:t.passBins,ringCount:t.ringCount}],population:e,testDefs:t.testDefs,activeTest:t.activeTest,notMeasuredReason:t.isLotStack?LOT_STACK_REASON:void 0}}export function selectionPopulation(a,e){return s(a,e.waferLabel?`selected on ${e.waferLabel}`:"selected",e)}export function waferPopulation(a,e){return s(a,e.waferLabel?`on ${e.waferLabel}`:"on this wafer",e)}
@@ -59,6 +59,13 @@ export interface ChartPanel {
59
59
  * instead, via `drill.onOpenGroup` — this is never called for those.
60
60
  */
61
61
  onOpen?: (datum: ChartDatum) => void;
62
+ /**
63
+ * Clicking a bar picks out the dies it counts (a bin's dies, in the bin pareto): called with the bar and the click, so
64
+ * the host can open a menu there. Takes the place of `onOpen` for panels whose bars are not wafers. `selectLabel` is
65
+ * what the click does, in the reader's words ("chart or tabulate these dies").
66
+ */
67
+ onSelectBar?: (datum: ChartDatum, e: MouseEvent) => void;
68
+ selectLabel?: string;
62
69
  /** Right-click on a leaf row that carries a wafer (`datum.key`) — see
63
70
  * `WaferContextMenuHandler`. Not called for a group row. */
64
71
  onWaferContextMenu?: WaferContextMenuHandler;
@@ -1 +1 @@
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 ut,makeSegmented as dt,fitRowsHeight as ht,setChartGrow as mt,resolveChartCanvasColors as xt,PADDING as u,VALUE_WIDTH as j,WAFER_MENU_HINT as bt,prepareCanvas as pt}from"./chartShell.js";import{escHtml as X}from"../../core/utils.js";const d=24,m=5,gt=15,z=110,yt=12;export function renderBarPanel(l,U){const{barColor:G,valueLabel:I}=l;let g=l.title,i=l.data;const{card:r,heading:E,controlsRow:P,body:S}=rt(g,U,l.ownerDocument);if(mt(r,"rows"),l.selfControl){const e=l.selfControl;P.appendChild(dt(e.options,e.current,n=>q(n),r.ownerDocument))}const{drill:f}=l;let y=!1,R=null;const A=r.ownerDocument.createElement("div");Object.assign(A.style,{color:it.label,fontSize:lt.body,marginBottom:nt.sm}),r.insertBefore(A,S);function B(){const e=[];l.onOpen&&e.push("click to open this wafer"),f&&!y&&e.push(`click a ${f.groupLabelText} to see it by wafer`);const n=e.join(", or ");let o=n?`${n[0].toUpperCase()}${n.slice(1)}.`:"";const t=l.reference?.(i);t&&(o=o?`${o} Dashed line: ${t.label}.`:`Dashed line: ${t.label}.`),A.textContent=o}B();const k=r.ownerDocument.createElement("div"),v=()=>l.reference?gt:0,Y=()=>u*2+v()+Math.min(i.length,yt)*(d+m);Object.assign(k.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${Y()}px`,scrollbarGutter:"stable"}),S.appendChild(k);const s=r.ownerDocument.createElement("canvas");s.style.display="block",s.style.cursor="default",k.appendChild(s);const w=at(r);let x=-1,M=Math.max(1,...i.map(e=>e.value));const N=e=>I?I(e):`${st(e.value)} (${e.percent.toFixed(1)}%)`;function F(e){const n=u+v()+e*(d+m),o=u+z,t=s.clientWidth-o-j-u;return{y:n,barX:o,barMaxWidth:Math.max(10,t)}}function C(){const e=u*2+v()+i.length*(d+m);k.style.maxHeight=`${ht(r,S,Y(),e)}px`;const n=k.clientWidth,o=pt(s,r,n,e);if(!o)return;const{ctx:t}=o;t.font=`${ot(-1)}px system-ui, sans-serif`,t.textBaseline="middle";const c=xt(r);i.forEach((a,p)=>{const{y:h,barX:W,barMaxWidth:H}=F(p);p===x&&(t.fillStyle=c.bgHover,t.fillRect(0,h-m/2,n,d+m)),t.fillStyle=c.text,t.textAlign="right";const T=a.label.length>16?`${a.label.slice(0,15)}\u2026`:a.label;t.fillText(T,u+z-8,h+d/2),t.fillStyle=c.track,t.fillRect(W,h,H,d);const $=Math.max(1,a.value/M*H);t.fillStyle=G?G(a,p):c.text,t.fillRect(W,h,$,d),t.fillStyle=c.text,t.textAlign="right",t.fillText(N(a),W+H+j,h+d/2)});const b=l.reference?.(i);if(b&&i.length){const{barX:a,barMaxWidth:p}=F(0),h=Math.round(a+Math.max(0,Math.min(1,b.value/M))*p)+.5,W=u+v()-3,H=u+v()+i.length*(d+m)-m,T=(Z,tt,et)=>{t.save(),t.strokeStyle=Z,t.lineWidth=tt,t.setLineDash(et),t.beginPath(),t.moveTo(h,W),t.lineTo(h,H),t.stroke(),t.restore()};T(c.bg,3.5,[]),T(c.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(b.label).width+$*2,_=13,V=u+v()-_-1;let D=h-O/2;D=Math.max(a,Math.min(D,a+p-O)),t.fillStyle=c.bg,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.rect(D,V,O,_),t.fill(),t.stroke(),t.fillStyle=c.text,t.fillText(b.label,D+$,V+_/2+.5),t.restore()}}function q(e){if(!l.selfControl)return;l.selfControl.current=e;const n=l.selfControl.onChange(e);i=n.data,M=Math.max(1,...i.map(o=>o.value)),x=-1,n.title&&(g=n.title,E.textContent=g),B(),C()}function J(e){if(!f)return;const n=f.onOpenGroup(e);i=n.data,g=n.title,E.textContent=g,M=Math.max(1,...i.map(o=>o.value)),x=-1,y||(y=!0,R=ut(()=>K(),r.ownerDocument),P.appendChild(R)),B(),C()}function K(){if(!f)return;const e=f.onBack();i=e.data,g=e.title,E.textContent=g,M=Math.max(1,...i.map(n=>n.value)),x=-1,y=!1,R?.remove(),R=null,B(),C()}function L(e){const n=Math.floor((e-u+m/2)/(d+m));return n>=0&&n<i.length?n:-1}s.addEventListener("mousemove",e=>{const n=s.getBoundingClientRect(),o=L(e.clientY-n.top),t=o>=0&&f&&!y&&i[o].itemCount>1,c=o>=0&&(t||!!l.onOpen&&o>=0&&!t);if(o!==x&&(x=o,s.style.cursor=c?"pointer":"default",C()),o>=0){const b=i[o],a=t?`<br><em>click to see this ${X(f.groupLabelText)} by wafer</em>`:l.onOpen?"<br><em>click to open this wafer</em>":"",p=!t&&l.onWaferContextMenu&&typeof b.key=="number"?bt:"";w.innerHTML=`<strong>${X(b.label)}</strong><br>${X(N(b))}${a}${p}`,w.style.display="block",ft(w,r,e.clientX,e.clientY)}else w.style.display="none"}),s.addEventListener("mouseleave",()=>{x!==-1&&(x=-1,C()),w.style.display="none"}),s.addEventListener("click",e=>{const n=s.getBoundingClientRect(),o=L(e.clientY-n.top);if(o===-1)return;const t=i[o];if(f&&!y&&t.itemCount>1){J(t);return}l.onOpen?.(t)}),s.addEventListener("contextmenu",e=>{const n=s.getBoundingClientRect(),o=L(e.clientY-n.top);if(o===-1||!l.onWaferContextMenu)return;const t=i[o];f&&!y&&t.itemCount>1||typeof t.key!="number"||(w.style.display="none",l.onWaferContextMenu(t.key,void 0,e))});const Q=ct(r,()=>C());return C(),{card:r,destroy:()=>Q.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 ut,makeSegmented as dt,fitRowsHeight as ht,setChartGrow as mt,resolveChartCanvasColors as xt,PADDING as u,VALUE_WIDTH as j,WAFER_MENU_HINT as bt,prepareCanvas as pt}from"./chartShell.js";import{escHtml as D}from"../../core/utils.js";const d=24,m=5,yt=15,z=110,gt=12;export function renderBarPanel(l,U){const{barColor:G,valueLabel:I}=l;let y=l.title,i=l.data;const{card:r,heading:E,controlsRow:P,body:A}=rt(y,U,l.ownerDocument);if(mt(r,"rows"),l.selfControl){const e=l.selfControl;P.appendChild(dt(e.options,e.current,n=>q(n),r.ownerDocument))}const{drill:f}=l;let g=!1,W=null;const H=r.ownerDocument.createElement("div");H.dataset.wmapCaption="1",Object.assign(H.style,{color:it.label,fontSize:lt.body,marginBottom:nt.sm}),r.insertBefore(H,A);function $(){const e=[];l.onOpen&&e.push("click to open this wafer"),l.onSelectBar&&e.push(`click a bar to ${l.selectLabel??"see what it counts"}`),f&&!g&&e.push(`click a ${f.groupLabelText} to see it by wafer`);const n=e.join(", or ");let o=n?`${n[0].toUpperCase()}${n.slice(1)}.`:"";const t=l.reference?.(i);t&&(o=o?`${o} Dashed line: ${t.label}.`:`Dashed line: ${t.label}.`),H.textContent=o}$();const k=r.ownerDocument.createElement("div"),w=()=>l.reference?yt:0,Y=()=>u*2+w()+Math.min(i.length,gt)*(d+m);Object.assign(k.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${Y()}px`,scrollbarGutter:"stable"}),A.appendChild(k);const s=r.ownerDocument.createElement("canvas");s.style.display="block",s.style.cursor="default",k.appendChild(s);const C=at(r);let x=-1,M=Math.max(1,...i.map(e=>e.value));const N=e=>I?I(e):`${st(e.value)} (${e.percent.toFixed(1)}%)`;function F(e){const n=u+w()+e*(d+m),o=u+z,t=s.clientWidth-o-j-u;return{y:n,barX:o,barMaxWidth:Math.max(10,t)}}function v(){const e=u*2+w()+i.length*(d+m);k.style.maxHeight=`${ht(r,A,Y(),e)}px`;const n=k.clientWidth,o=pt(s,r,n,e);if(!o)return;const{ctx:t}=o;t.font=`${ot(-1)}px system-ui, sans-serif`,t.textBaseline="middle";const c=xt(r);i.forEach((a,p)=>{const{y:h,barX:B,barMaxWidth:S}=F(p);p===x&&(t.fillStyle=c.bgHover,t.fillRect(0,h-m/2,n,d+m)),t.fillStyle=c.text,t.textAlign="right";const R=a.label.length>16?`${a.label.slice(0,15)}\u2026`:a.label;t.fillText(R,u+z-8,h+d/2),t.fillStyle=c.track,t.fillRect(B,h,S,d);const L=Math.max(1,a.value/M*S);t.fillStyle=G?G(a,p):c.text,t.fillRect(B,h,L,d),t.fillStyle=c.text,t.textAlign="right",t.fillText(N(a),B+S+j,h+d/2)});const b=l.reference?.(i);if(b&&i.length){const{barX:a,barMaxWidth:p}=F(0),h=Math.round(a+Math.max(0,Math.min(1,b.value/M))*p)+.5,B=u+w()-3,S=u+w()+i.length*(d+m)-m,R=(Z,tt,et)=>{t.save(),t.strokeStyle=Z,t.lineWidth=tt,t.setLineDash(et),t.beginPath(),t.moveTo(h,B),t.lineTo(h,S),t.stroke(),t.restore()};R(c.bg,3.5,[]),R(c.text,1.5,[4,3]),t.save(),t.font="600 10px system-ui, sans-serif",t.textBaseline="middle",t.textAlign="left";const L=4,_=t.measureText(b.label).width+L*2,X=13,V=u+w()-X-1;let T=h-_/2;T=Math.max(a,Math.min(T,a+p-_)),t.fillStyle=c.bg,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.rect(T,V,_,X),t.fill(),t.stroke(),t.fillStyle=c.text,t.fillText(b.label,T+L,V+X/2+.5),t.restore()}}function q(e){if(!l.selfControl)return;l.selfControl.current=e;const n=l.selfControl.onChange(e);i=n.data,M=Math.max(1,...i.map(o=>o.value)),x=-1,n.title&&(y=n.title,E.textContent=y),$(),v()}function J(e){if(!f)return;const n=f.onOpenGroup(e);i=n.data,y=n.title,E.textContent=y,M=Math.max(1,...i.map(o=>o.value)),x=-1,g||(g=!0,W=ut(()=>K(),r.ownerDocument),P.appendChild(W)),$(),v()}function K(){if(!f)return;const e=f.onBack();i=e.data,y=e.title,E.textContent=y,M=Math.max(1,...i.map(n=>n.value)),x=-1,g=!1,W?.remove(),W=null,$(),v()}function O(e){const n=Math.floor((e-u+m/2)/(d+m));return n>=0&&n<i.length?n:-1}s.addEventListener("mousemove",e=>{const n=s.getBoundingClientRect(),o=O(e.clientY-n.top),t=o>=0&&f&&!g&&i[o].itemCount>1,c=o>=0&&(t||(!!l.onOpen||!!l.onSelectBar)&&!t);if(o!==x&&(x=o,s.style.cursor=c?"pointer":"default",v()),o>=0){const b=i[o],a=t?`<br><em>click to see this ${D(f.groupLabelText)} by wafer</em>`:l.onOpen?"<br><em>click to open this wafer</em>":l.onSelectBar?`<br><em>click to ${D(l.selectLabel??"see what it counts")}</em>`:"",p=!t&&l.onWaferContextMenu&&typeof b.key=="number"?bt:"";C.innerHTML=`<strong>${D(b.label)}</strong><br>${D(N(b))}${a}${p}`,C.style.display="block",ft(C,r,e.clientX,e.clientY)}else C.style.display="none"}),s.addEventListener("mouseleave",()=>{x!==-1&&(x=-1,v()),C.style.display="none"}),s.addEventListener("click",e=>{const n=s.getBoundingClientRect(),o=O(e.clientY-n.top);if(o===-1)return;const t=i[o];if(f&&!g&&t.itemCount>1){J(t);return}if(l.onSelectBar){C.style.display="none",l.onSelectBar(t,e);return}l.onOpen?.(t)}),s.addEventListener("contextmenu",e=>{const n=s.getBoundingClientRect(),o=O(e.clientY-n.top);if(o===-1||!l.onWaferContextMenu)return;const t=i[o];f&&!g&&t.itemCount>1||typeof t.key!="number"||(C.style.display="none",l.onWaferContextMenu(t.key,void 0,e))});const Q=ct(r,()=>v());return v(),{card:r,destroy:()=>Q.disconnect()}}
@@ -1,4 +1,4 @@
1
- import { type BinItem } from '../../stats/binPareto.js';
1
+ import { type BinItem, type BinType } from '../../stats/binPareto.js';
2
2
  import { type SaveImageHandler } from './chartShell.js';
3
3
  export interface BinClusterPanelOptions {
4
4
  title?: string;
@@ -7,6 +7,12 @@ export interface BinClusterPanelOptions {
7
7
  items: BinItem[];
8
8
  }[];
9
9
  onSaveImage?: SaveImageHandler;
10
+ /** Click a sub-bar: a bin in one group. The host picks out those dies and opens a menu at the click. */
11
+ onSelectBin?: (sel: {
12
+ bin: number;
13
+ binType: BinType;
14
+ groupKey: string;
15
+ }, e: MouseEvent) => void;
10
16
  /** Document to build this panel's DOM into. Default `document` — pass the
11
17
  * host's own `ownerDocument` when the container might live in a
12
18
  * different document (e.g. a gallery card detached into its own popup