@wafertools/wafermap 0.31.0 → 0.33.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 (100) hide show
  1. package/AGENTS.md +13 -13
  2. package/CHANGELOG.md +334 -1
  3. package/README.md +3 -3
  4. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +2 -12
  5. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  6. package/dist/packages/canvas-adapter/dieList.js +7 -7
  7. package/dist/packages/canvas-adapter/drilldown.js +1 -1
  8. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  9. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  10. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +47 -4
  11. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  12. package/dist/packages/canvas-adapter/summaryPanel.d.ts +22 -3
  13. package/dist/packages/canvas-adapter/summaryPanel.js +4 -3
  14. package/dist/packages/canvas-adapter/toCanvas.d.ts +23 -2
  15. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  16. package/dist/packages/canvas-adapter/toolbar.d.ts +54 -1
  17. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  18. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  19. package/dist/packages/canvas-adapter/userGuideHtml.js +100 -27
  20. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  21. package/dist/packages/canvas-adapter/version.js +1 -1
  22. package/dist/packages/core/aggregates.d.ts +6 -0
  23. package/dist/packages/core/aggregates.js +1 -1
  24. package/dist/packages/core/compact.d.ts +205 -0
  25. package/dist/packages/core/compact.js +3 -0
  26. package/dist/packages/core/dieTable.d.ts +137 -0
  27. package/dist/packages/core/dieTable.js +1 -0
  28. package/dist/packages/core/dies.d.ts +22 -0
  29. package/dist/packages/core/dies.js +1 -1
  30. package/dist/packages/core/passBins.d.ts +6 -0
  31. package/dist/packages/core/passBins.js +1 -1
  32. package/dist/packages/core/utils.d.ts +14 -0
  33. package/dist/packages/core/utils.js +1 -1
  34. package/dist/packages/renderer/axisTicks.d.ts +14 -0
  35. package/dist/packages/renderer/axisTicks.js +1 -1
  36. package/dist/packages/renderer/buildView.d.ts +29 -4
  37. package/dist/packages/renderer/buildView.js +1 -1
  38. package/dist/packages/renderer/buildWaferMap.d.ts +29 -11
  39. package/dist/packages/renderer/buildWaferMap.js +1 -1
  40. package/dist/packages/renderer/columnarInput.d.ts +87 -0
  41. package/dist/packages/renderer/columnarInput.js +1 -0
  42. package/dist/packages/renderer/compactView.d.ts +22 -0
  43. package/dist/packages/renderer/compactView.js +1 -0
  44. package/dist/packages/renderer/derivedTests/apply.d.ts +8 -6
  45. package/dist/packages/renderer/derivedTests/apply.js +1 -1
  46. package/dist/packages/renderer/index.d.ts +1 -0
  47. package/dist/packages/renderer/spec.d.ts +3 -7
  48. package/dist/packages/renderer/spec.js +1 -1
  49. package/dist/packages/renderer/xyIndicator.d.ts +27 -0
  50. package/dist/packages/renderer/xyIndicator.js +1 -0
  51. package/dist/packages/stats/analyzeWaferLot.d.ts +28 -0
  52. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  53. package/dist/packages/stats/analyzeWaferMap.d.ts +169 -2
  54. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  55. package/dist/packages/stats/binRows.d.ts +38 -0
  56. package/dist/packages/stats/binRows.js +1 -0
  57. package/dist/packages/stats/boxplot.js +1 -1
  58. package/dist/packages/stats/capability.d.ts +7 -7
  59. package/dist/packages/stats/capability.js +1 -1
  60. package/dist/packages/stats/clusterDetection.js +1 -1
  61. package/dist/packages/stats/connectedComponents.js +1 -1
  62. package/dist/packages/stats/correlation.js +1 -1
  63. package/dist/packages/stats/filterFindings.d.ts +30 -0
  64. package/dist/packages/stats/filterFindings.js +1 -1
  65. package/dist/packages/stats/findingText.d.ts +9 -0
  66. package/dist/packages/stats/findingText.js +2 -0
  67. package/dist/packages/stats/histogram.js +1 -1
  68. package/dist/packages/stats/lotDrift.d.ts +16 -0
  69. package/dist/packages/stats/lotDrift.js +1 -0
  70. package/dist/packages/stats/lotPattern.d.ts +33 -0
  71. package/dist/packages/stats/lotPattern.js +1 -0
  72. package/dist/packages/stats/math.d.ts +73 -6
  73. package/dist/packages/stats/math.js +1 -1
  74. package/dist/packages/stats/normalizeInput.js +1 -1
  75. package/dist/packages/stats/patternClassification.d.ts +34 -3
  76. package/dist/packages/stats/patternClassification.js +1 -1
  77. package/dist/packages/stats/presentation.d.ts +42 -0
  78. package/dist/packages/stats/presentation.js +1 -0
  79. package/dist/packages/stats/regions.d.ts +16 -0
  80. package/dist/packages/stats/regions.js +1 -1
  81. package/dist/packages/stats/renderSummaryReport.d.ts +15 -2
  82. package/dist/packages/stats/renderSummaryReport.js +32 -27
  83. package/dist/packages/stats/reportHtml.d.ts +54 -9
  84. package/dist/packages/stats/reportHtml.js +130 -47
  85. package/dist/packages/stats/scatter.js +1 -1
  86. package/dist/packages/stats/sweep.js +1 -1
  87. package/dist/packages/stats/synthesis.d.ts +66 -0
  88. package/dist/packages/stats/synthesis.js +2 -0
  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 +6 -1
  92. package/dist/packages/stats/yieldRows.d.ts +31 -0
  93. package/dist/packages/stats/yieldRows.js +1 -0
  94. package/dist/packages/worker/index.js +1 -1
  95. package/dist/packages/worker/wafermap.worker.d.ts +3 -0
  96. package/dist/packages/worker/wafermap.worker.js +1 -1
  97. package/llms.txt +1 -1
  98. package/package.json +1 -1
  99. package/dist/packages/stats/findingsNarrative.d.ts +0 -3
  100. package/dist/packages/stats/findingsNarrative.js +0 -1
package/AGENTS.md CHANGED
@@ -45,12 +45,11 @@ worth reporting, not a pattern to build on.
45
45
  pre-multiply. The geometry inputs are `waferConfig` (type `WaferConfig`) and
46
46
  `dieConfig` (type `DieConfig`) — both optional, both inferred when omitted.
47
47
  - **Bins and test values must be numbers, and verdicts booleans.** Every parser —
48
- CSV, JSON, a spreadsheet export — hands you `"1"`, and `"1"` is not pass bin 1: those
49
- dies count as fails and the yield is wrong, while a test value left as text is not
50
- plotted or analysed correctly. Convert with `Number()` at parse time. `buildWaferMap`
51
- samples the input and reports `input-values-not-numbers` (severity `'error'`) rather
52
- than coercing behind your back, so a build that "works" can still be wrong — read the
53
- 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.
54
53
  - **`passBins` and `ringCount` are set once, on `buildWaferMap`, and travel on the
55
54
  result.** Neither is an option on `analyzeWaferMap`, `analyzeWaferLot`,
56
55
  `renderWaferMap` or `renderWaferGallery` — passing one there is a type error in
@@ -137,9 +136,9 @@ worth reporting, not a pattern to build on.
137
136
  but `'traffic'` and `'jet'` reads low = dark, high = light; if you draw your own
138
137
  colorbar or swatch, resolve the colour through `resolveValueColorFn(name, reversed)`
139
138
  so it cannot disagree with the dies.
140
- - `result.view` is internal. Use the promoted fields: `result.plotMode`,
141
- `result.metadata`, `result.isLotStack`, `result.hbinDefs`, `result.sbinDefs`,
142
- `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.
143
142
  - Die keys come from `getDieKey(die)`. A hand-rolled `` `${x},${y}` `` breaks
144
143
  click-to-highlight silently, because findings carry `dieKeys` in that exact format.
145
144
  - `stats.warnings` is `WaferWarning[]` (it was `string[]` before 0.22.0). Read
@@ -180,9 +179,10 @@ it is handed, because it has no way to know which tests anyone will look at.
180
179
  `computePerTestStats` is modest; `enableTestValueAnalysis` is the expensive one
181
180
  (roughly 10× the base analysis on a large wafer) and exists to find spatial
182
181
  patterns automatically — do not enable it by default just because it sounds good.
183
- - **A Web Worker buys responsiveness, not speed.** `createWafermapWorker` copies data
184
- across `postMessage`, so total time goes *up*. Use it when a build would otherwise
185
- 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.
186
186
  - **Capability, pass rates, region yield and the spatial-pattern label come back from
187
187
  the analysis** — `stats.capability` (with `computePerTestStats`), `stats.testSpecYield`,
188
188
  `stats.testFlagYield`, `stats.functionalYield`, `stats.regionYield` and
@@ -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,339 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
25
+ ## [0.33.0] — 2026-10-02
26
+
27
+ ### Breaking
28
+
29
+ - **`sectorCount` is 4, 8 or 16.** The compass names cover 16 bearings, so 32 has no names to give its sectors;
30
+ it is corrected to the default with an `'analysis-option-corrected'` warning, like any other unusable value.
31
+
32
+ ### Changed
33
+
34
+ - **An expanded map's window widens for the Summary panel.** Opening the panel in a map's Expand modal, or in a wafer
35
+ opened from a gallery, makes the window wider by the room the panel needs, so the map keeps its size instead of
36
+ giving up more than half its width; closing the panel narrows it again. The window stays within the screen (a
37
+ floating window moves left if it would run off the edge), does nothing when maximised, and leaves a width the user
38
+ has dragged it to. A wafer opened in a separate browser window is resized the same way where the browser allows it.
39
+ - **The report builders load when a report is opened.** The Summary panel's report button and the gallery's load
40
+ `renderSummaryReport` with a dynamic `import()` instead of with the map, which takes about 10 KB gzipped out of what
41
+ rendering a map downloads (core is ~143 KB with this release's additions); the first report opens a moment later while it loads.
42
+ `renderWaferReportHtml` and `renderLotReportHtml` are unchanged public exports.
43
+ - **The Summary panel leads into the report, and the report acts on the map.** "What stands out" ends with a **Full
44
+ report** link that opens the same report the Summary report button does, which begins with that same section. In
45
+ a report opened from the panel, a click on a finding's row closes it and shows that finding on the map, as its
46
+ row in the panel does (on a lot report that is split into several groups, a finding the panel does not hold is
47
+ ignored). Opened on its own, the report is a plain page and its rows do nothing.
48
+ - **The reports and the Summary panel count and order bins, findings and statistics the same way.** One count of dies
49
+ per bin (partial and edge-excluded dies left out) and one order (pass bins first, then fail bins by count) feed the
50
+ panel's bars and the reports' tables, under one title that states the population ("Hard Bin Breakdown — % of dies
51
+ (N=…)"); the reports show the soft-bin breakdown as well when the data has soft bins, in the same columns for a
52
+ wafer and a lot. The reports' findings table is arranged as the panel's list is: each spatial pattern with the
53
+ findings it explains beneath it, then the rest by region, most severe first. The wafer report's Test Values table
54
+ has the panel's full columns (N, min, quartiles, median, mean, max, σ) and the share of dies inside each test's
55
+ limits; a lot's has N, min, mean, max, σ (pooled exactly) and limit yield, and says why it has no quartiles. The
56
+ lot overview names the unweighted mean of wafer yields "Mean per-wafer yield" everywhere.
57
+ - **The Summary panel and the HTML reports read yields the same way.** One computation gives each wafer's
58
+ difference from the lot median (with the lot's outlier rule naming the outliers) and each ring or quadrant's
59
+ difference from the wafer or lot it divides. The panel's wafer and region rows print that difference in points
60
+ and tint a shortfall of 1, 2 or 4 or more points, as the report's tables do; the report's wafer table says
61
+ "low outlier" in words, as the panel always has. The panel's findings mark severity as three dots filled by level
62
+ rather than a colour-only dot, as the reports do. The reports draw bin bars in the colour the map gives that bin
63
+ (the live colours when opened from a panel, the default palette otherwise).
64
+ - **The Summary panel opens with "What stands out"**, the same synthesis the HTML reports open with (a headline, up to
65
+ three items ranked by the dies each costs, an "also" line, and what was compared), in place of the findings
66
+ narrative above the findings list. It is a section of its own, so a clean wafer or lot says "Nothing stands out"
67
+ where the findings list is absent. The names in it (a region, a bin) select that finding on the map; the Findings
68
+ section's Detail button opens the same sentences above the full list. Where the wafers in a lot were judged by
69
+ different pass bins, the items rest on yield alone and name no pass bins.
70
+ - **The weakest finding severity is called "Minor" where it is shown** (the Summary panel's severity filter and the
71
+ reports); the value in `StatsFinding.severity` is unchanged.
72
+ - **A merged region on a wafer is graded with its constituents' multiple-testing correction.** The merged finding's
73
+ p-value is multiplied by the weakest constituent's adjusted-to-raw ratio before severity is assigned, the same rule the
74
+ lot merge applies, and `stats.adjustedPValue` is set. A merge can no longer grade more severe than the correction its
75
+ parts carry allows, so a few merged findings show a lower severity.
76
+ - **A lot stack by standard deviation or count carries no test limits.** Its values are spreads and tallies, not
77
+ measurements of the test, so `result.testDefs` has the limits removed and nothing is judged against them: no
78
+ limit fail findings, limit yield, capability index or out-of-spec colouring. Stacks by mean, median, minimum
79
+ and maximum keep them.
80
+ - **A sector run and a quadrant over the same part of the wafer are one finding.** "Sectors W–S" and
81
+ "Quadrant SW" make one statement, so the one with the smaller p-value is shown and the other is listed
82
+ under it (`absorbedIds`). They must be the same comparison (metric, variable and direction) and cover
83
+ angles that overlap by at least three fifths of their union, by geometry. A single sector inside a
84
+ quadrant, and a mean against a limit fail rate, stay separate. Wafer and lot findings follow the same rule.
85
+ - **Lot findings merge adjacent regions.** A run of adjacent sectors, quadrants or rings that carries one
86
+ signal is one lot finding ("Sectors E–N", "Rings 1–2"), as on a single wafer. Each wafer's regions are
87
+ added up, the rest being the wafer's other regions, and those per-wafer figures are combined across the
88
+ wafers as the single regions are (Stouffer's Z, the same gates). The run is adjusted with the
89
+ Benjamini–Hochberg multiplier of its weakest region and is reported only if it passes on its own;
90
+ otherwise its separate rows stay. "N/M wafers" counts the wafers where the merged region itself differs at
91
+ the analysis's significance level.
92
+ - **A merged run is compared with the rest of its region family.** As for each region on its own: sectors
93
+ leave out the centre dies, so a merged sector run's "rest of the map" is the other sectors' dies.
94
+ - **Adjacent regions merge per metric.** A test's mean finding and its limit fail rate are kept apart when
95
+ adjacent sectors, quadrants and rings are merged, so a run such as sectors E, NE and N reads as one
96
+ finding ("Sectors E–N"), and a quadrant with both a mean and a limit finding lists both. A run of limit
97
+ fail rates merges too, recomputed over the union of its regions. A run of quadrants is named in order
98
+ round the wafer.
99
+ - **A lot's spatial pattern is decided from the lot.** `analyzeWaferLot` stacks the lot's wafers by die
100
+ position, finds the positions where failures recur (an exact binomial test on each position's 3×3
101
+ neighbourhood, Benjamini–Hochberg across positions), classifies that area with the same classifier and
102
+ thresholds as a single wafer, and reports the pattern once, on every wafer whose failures are concentrated
103
+ in it. A pattern visible on every wafer is reported on every wafer under one label, with the ring, edge-arc
104
+ and cluster findings it explains listed under it. Wafers on a different grid, and lots too small for
105
+ failures to recur significantly, keep the per-wafer count. The wafer-level classifier and its thresholds
106
+ are unchanged (WM-811K: 64.1% overall).
107
+ - **Outlier wafers have one rule**, read by both the lot findings and the Summary panel's
108
+ Wafer Yield list, so they always name the same wafers. From 3 to 7 wafers it is Dixon's Q
109
+ test on the lowest and highest wafer (95% → `notable`, 99% → `unusual`); from 8 wafers,
110
+ Tukey's fences over per-wafer yield (1.5 × IQR → `notable`, 3 × IQR → `unusual`). Either
111
+ way the wafer must also be at least 3 yield points from the median. The
112
+ finding's `stats.method` is `'dixon-q'` or `'tukey-fence'`, and `effect.effectSize` is
113
+ that test's statistic (Q, or the distance from the median in IQR units). The Wafer Yield
114
+ list labels high outliers as well as low ones.
115
+
116
+ ### Added
117
+
118
+ - **Compact layout for multi-project wafers.** A **Compact layout** row in the Overlays menu, on a single map and on the
119
+ gallery, draws the dies on a grid with the empty rows and columns removed and each group of dies outlined, so a
120
+ sparse layout fills the map instead of leaving each die a few pixels wide. It is offered when the occupied columns
121
+ and rows repeat at a regular pitch, or at the `reticleConfig` width and height, or a multiple of it (a product on
122
+ every second reticle), when every wafer supplies the same ones; `viewOptions: { compact: true }` applies it regardless. Only the layout changes:
123
+ every die is still drawn and counted, so legends, yield and statistics are the same, and hover text and axis
124
+ labels give original die coordinates. A die with no bin keeps its place. The notch marker follows rotation and
125
+ flips, and so does the XY indicator, which sits in a margin beside the grid. A gallery builds one layout from every
126
+ wafer shown, so its cards stay comparable. The wafer outline, ring, quadrant and reticle overlays describe the
127
+ physical wafer and are not drawn in this layout.
128
+ - **Rotating or flipping a map switches the XY indicator on.** The first Rotate or Flip from the Orientation menu, on a
129
+ single map or a gallery, turns on the +X/+Y arrows in whatever layout is showing, so the reader can see which way
130
+ the die coordinates run. They stay on until switched off in the Overlays menu, and **Reset orientation** does not
131
+ change them.
132
+ - **An Axis labels row in the Overlays menu.** The die-coordinate labels along the bottom and left edges appear when the
133
+ map is zoomed, as before; the row shows them always, or hides them, and **Clear overlays** returns it to that default.
134
+ `viewOptions: { showAxes }` is the same choice for a host. In the compact layout the labels name the first column
135
+ or row of each group of dies, which are the reticle boundaries, and every die once the cells are wide enough to
136
+ label them all.
137
+ - **Gallery cards fit the compact layout.** With the compact layout on, a card is as tall as its map needs and as wide
138
+ as that layout calls for, rather than sized for the wafer it replaces, so more cards fit on screen. A quarter turn
139
+ swaps the card's width and height. Turning the layout off restores the square card.
140
+ - **A multi-project wafer example and guide section.** The new Multi-project wafers example page shows the compact
141
+ layout on a synthetic wafer where two products share a reticle, and the guide's "Compact layout for multi-project
142
+ wafers" section covers the option, when it is offered, galleries, axis labels and the diagnostics.
143
+ - **Layout diagnostics.** A **Layout diagnostics** row beside it shows what the compact layout's detector saw: counts,
144
+ periods and scores only, with no die positions, bins, test values or wafer identity. **Copy** and **Save as file**
145
+ let someone report why a layout was or was not recognised without sharing their data.
146
+ - **A lot's yield or a test's mean can be reported as drifting across the wafers.** `analyzeWaferLot` runs a
147
+ Mann–Kendall trend test (with the Theil–Sen slope to size it) on the per-wafer yield, and on each test's per-wafer
148
+ mean when per-test statistics were computed, for lots of five or more wafers. The p-values are corrected together
149
+ (Benjamini–Hochberg), and a trend must also move far enough to matter: two points of yield, or half a within-wafer
150
+ σ of a test. A finding has id `drift:yield` or `drift:test:<number>`, level `'inter-wafer'`, family `'wafer'`,
151
+ method `'mann-kendall'`, and its summary says "(input order)": the wafers' order is a physical one only when the host
152
+ says so. The reports and the Summary panel list the strongest as a **Watch** line under "What stands out", with up to
153
+ 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
154
+ from the items, which are ranked by dies lost.
155
+ - **Both HTML reports open with a "What stands out" section.** A headline (the yield, and for a lot its
156
+ spread across wafers), up to three items ranked by the dies each costs, and a line saying what was
157
+ compared. An item is a region whose pass rate fell, a fail bin that rose, a wafer well below the lot, a
158
+ parametric test outside its limits on a share of the dies, or a functional test that fails;
159
+ findings standing on the same dies are one item, a repeated spatial pattern leads its item, and the fail
160
+ bins that make up a region's shortfall are named with their share. An item's impact (High, Moderate or Low) is the
161
+ 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
162
+ is most of a good lot's loss reads as high impact. Only an item costing at least one
163
+ yield point of the analysed dies is listed; otherwise the section says nothing stands out. Each figure links
164
+ to its row in the Findings table, which now carries an anchor per row (a test item names the test and
165
+ has no row to link to).
166
+ - **Both HTML reports have a new layout.** Severity is three dots and a word (Unusual, Notable, Minor) in place of
167
+ the coloured badge, so it reads in a black-and-white print and for colour-blind readers; findings rows and the
168
+ items in "What stands out" carry a light tint and a left edge. Yields, regions, wafers and bins are drawn with a
169
+ bar behind the figure, and a wafer or region is tinted only when it is 1, 2 or 4 or more points below its
170
+ reference, so a lot with nothing wrong has no tint. The key figures are tiles, pass bins are written in grey, and
171
+ a contents line leads the page (not printed). Items over the three shown go on one "Also over a yield point" line.
172
+
173
+ ### Fixed
174
+
175
+ - An outlier wafer's finding reports its yield difference as a fraction in `effect.absoluteDelta`, like every
176
+ other yield finding, so the findings table shows "−4.3 pp" for a wafer 4.3 points below the lot median.
177
+ - The spatial-pattern classifier calls a failing region a scratch only when it reaches beyond
178
+ the central zone (35% of the radius). An elongated cluster wholly inside it is a centre
179
+ cluster. On the WM-811K benchmark (25,519 wafers): centre recall 59.9% → 60.5%, scratch
180
+ precision 33.3% → 35.7%, scratch recall 24.3% → 24.1%, overall 64.0% → 64.1%.
181
+ - A lot's repeated spatial pattern lists, as its `relatedIds`, the repeated findings that
182
+ each wafer's own pattern claimed on every wafer they appear on — so an edge arc that is part
183
+ of the edge ring on each of its wafers is shown under the lot's edge pattern, as at wafer
184
+ level, not as a separate lot finding.
185
+
186
+ ## [0.32.0] — 2026-09-27
187
+
188
+ ### Breaking
189
+
190
+ - **Input values of the wrong type are treated as missing.** A bin, site number or test value
191
+ that is not a number, or a pass/fail verdict that is not `true`/`false`, is left out of the
192
+ die — the same rule as a value outside the STDF V4 ranges — and still reported as
193
+ `input-values-not-numbers`. A die whose only bin was text therefore has no verdict rather
194
+ than a fail, which can change `yield` for such input. The input objects are not modified.
195
+ - **Test values on dies from `buildWaferMap` are read-only snapshots.** A map holds test values and
196
+ verdicts as one column per test. `die.testValues` and `die.testPass` on a built die build a frozen
197
+ object from those columns on each read, keeping nothing on the die, so `die.testValues !==
198
+ die.testValues`. Assigning to either field throws a `TypeError`; changing a key of the frozen
199
+ object throws in strict-mode code and is ignored otherwise. Pass the values to `buildWaferMap`,
200
+ or copy the die with your own object (`{ ...die, testValues: mine }`). A spread, `structuredClone` or `JSON.stringify` of a built die
201
+ gives plain objects with the same values. A die no longer shares the input record's
202
+ `testValues`/`testPass` objects, so changing the input after the build does not change the map.
203
+ Dies a host builds itself keep ordinary objects.
204
+ - **Pre-built `dies` get the same input checks as `results`.** Wrong-type and out-of-range
205
+ values are treated as missing, and a die whose coordinates STDF cannot store is kept as an
206
+ unpositioned die.
207
+ - **`WaferMapResult.view` is removed.** It was marked `@internal`: the renderers build their own
208
+ draw list whenever they draw, so a result no longer carries one. Read the result's own fields
209
+ (`plotMode`, `metadata`, `isLotStack`, `hbinDefs`, `sbinDefs`, `testDefs`). This makes each
210
+ result smaller (about 86 bytes less per die) and roughly halves the copy the Web Worker makes of
211
+ a result. The internal `dataAxisFlip` field takes its place.
212
+
213
+ ### Changed
214
+
215
+ - **The legend filters to several bins or metadata values at once.** Ctrl/Cmd+click on a legend
216
+ entry — the map's legend or the gallery's strip, where Ctrl/Cmd+Enter/Space works from the
217
+ keyboard — adds or removes a value; a plain click shows only that value, or clears the
218
+ filter when it is the only one shown. `highlightBin` accepts `number | number[]` and
219
+ `highlightMetadataValue` accepts `string | string[]`.
220
+ - **A finding and the legend filter agree.** Clicking a finding about a bin filters the legend
221
+ to that bin, and one about yield or a test clears the filter — in a single map and in the
222
+ gallery alike. Changing the legend filter releases the finding, as changing the selection
223
+ does, including a selection made on a gallery card while a lot finding is active.
224
+ - **Lot regional findings are tested on all wafers' data together.** A yield, bin,
225
+ functional pass-rate, limit-fail or test-value difference in a region is combined across
226
+ every wafer (Stouffer's Z over each wafer's own test, weighted by die count) and reported
227
+ with the wafer analysis's gates, redundancy collapse and opposite-region re-test, instead of
228
+ by counting wafers whose own analysis reported it — a pattern present on every wafer but
229
+ too faint on some to pass alone is the lot's pattern. The sentence gives the lot's figure
230
+ and "higher/lower on N/M wafers, all wafers' data combined", N counting the wafers whose
231
+ region differs in that direction; `stats.method` is `'stouffer-z'`. Clusters, edge arcs and
232
+ spatial-pattern labels are still counted by the wafers that report them. Lot findings keep
233
+ absorbed restatements in the list, marked by `absorbedIds`, as wafer findings do.
234
+ - **Selected dies are shown by fading the rest of the map.** Every unselected die is faded
235
+ towards the map background and the selection is outlined, so selected dies keep their full
236
+ colour and it is clear which dies are selected whatever the selection's shape — a block, a
237
+ ring, an edge arc. A finding highlighted from the Summary panel, a gallery lot finding and
238
+ `setSelection` are drawn the same way. The fade and outline sit under the axes and legend.
239
+ - **A map opens in select mode.** A drag draws a selection box; hold Space and drag, use the
240
+ arrow keys, or choose Pan in the toolbar to pan.
241
+ - **Clicking the only selected die again clears the selection.** Clicking a die inside a
242
+ larger selection still selects just that die.
243
+ - **Changing the selection on the map releases an active finding.** A click, box select,
244
+ right-click, Esc or `clearSelection` clears the Summary panel's active finding and its bin
245
+ highlight, so the panel never shows a finding the map no longer highlights.
246
+
247
+ ### Fixed
248
+
249
+ - **Regional findings report patterns confined to one region.** A bin, limit-fail or
250
+ functional-fail rate that is zero in the rest of the wafer and raised in a region (a bin
251
+ found only at the edge) counts as the largest relative change for the effect gate and for
252
+ severity, for every region family: rings, quadrants, sectors, reticle positions and test
253
+ sites. On a lot whose wafers carry bin 2 only at the edge, the lot finding reads "seen on
254
+ 8/8 wafers".
255
+ - **Yield and functional pass-rate findings are judged by their failure rate.** The relative
256
+ effect of a pass rate is measured on its failures: yield 98% → 94% is failures 2% → 6%, and
257
+ reaches the same verdict as the bin rate of the same dies.
258
+ - **A region is not reported as deviating only because another region deviates more.** A
259
+ finding opposite in direction to a stronger finding for the same variable and region family
260
+ must still hold when compared with the rest of the wafer without that region, so an edge
261
+ rich in a bin, a limit fail or high test values no longer makes the inner rings read as low
262
+ in it (or high in yield).
263
+ - **The soft bin that restates yield is absorbed into the yield finding**, as the hard pass bin
264
+ is: the one soft bin every passing die carries and no failing die does.
265
+ - **An edge ring made of scattered fails is classified as an edge ring.** The spatial-pattern
266
+ classifier recognises an edge ring from the failing dies' positions — most fails at the rim
267
+ and spread round at least 60% of it — when no connected group of fails is large enough to
268
+ judge shape by, and before the scratch rule, so a short run along the rim is not a scratch.
269
+ Against WM-811K: detection 86.4%, edge-ring recall 75% (precision 92%), scratch recall 24%.
270
+ - **Lot spatial-pattern rows count related labels together and state the lot's figures.**
271
+ Edge-ring and edge-local wafers count as one edge pattern, and centre and donut as one,
272
+ naming each label's wafer count ("Spatial pattern: edge (edge-ring on 5, edge-local on 2) —
273
+ seen on 7/13 wafers"); the sentence gives the lot's count, not any one wafer's confidence or figures.
274
+ The gallery highlights each counted wafer's failing dies.
275
+ - Findings, their severities and "seen on N wafers" counts can change.
276
+
277
+ ### Performance
278
+
279
+ - **Insights no longer blocks while its test statistics are computed.** The Overview's Test Values
280
+ table is built in slices, showing "Computing test statistics…" until it is ready, and when the
281
+ gallery's summary panel is computing the same statistics at the time, the two share the one
282
+ calculation instead of each doing it.
283
+ - **Gallery cards are drawn only when on screen.** A card below the fold keeps its map up to date
284
+ and is drawn as it scrolls into view. Printing and the gallery PNG draw every card first, so
285
+ both still show the whole gallery. On a 25-wafer, 266k-die lot, with 4 cards on screen:
286
+ switching to value mode takes 0.4 s in Chrome (was 1.1 s) and 0.5 s in WebKit (was 2.0 s);
287
+ the gallery opens in 1.8 s and 2.0 s.
288
+ - **The gallery draws about twice as fast in WebKit** (the desktop app on Linux and macOS, and
289
+ Safari). Die fills and outlines are drawn as many small canvas paths rather than one per
290
+ colour, which WebKit rasterises far faster. On a 25-wafer, 266k-die lot the gallery opens in
291
+ 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).
292
+ Chrome draws identical pixels; in WebKit only anti-aliased edge pixels differ.
293
+ - **The lot Summary report opens about five times faster on a large lot.** Its Test Values table
294
+ finds each test's minimum, mean and maximum in one pass instead of sorting every die's value.
295
+ On a 25-wafer, 266k-die lot the "Summary report" button takes 0.4 s in Chrome (was 6.0 s) and
296
+ 0.9 s from click to report in WebKit (was 4.7 s). The report is unchanged (compared as HTML).
297
+ Boxplot quartiles and the histogram's outlier range are found the same way as the Summary
298
+ panel's, by selection, with the same figures.
299
+ - **Test-value analysis (`enableTestValueAnalysis`) is two to three times faster on large lots.**
300
+ Spec-limit findings count each region's dies in one pass instead of re-reading every die for
301
+ every region, the test list is read column by column, and per-test statistics find their
302
+ quartiles by selection. Findings are unchanged (compared byte for byte on a 266k-die lot with
303
+ 5,289 findings). On that lot: 18.9 s → 7.2 s in Chrome, 25.6 s → 10.7 s in WebKit.
304
+ - **Analysis is about a third faster in Chrome on large lots.** Merging findings in adjacent
305
+ regions, pairing hard and soft bins that cover the same dies, and assigning dies to regions
306
+ now compare die positions as numbers rather than as a text key per die. Findings are
307
+ unchanged (compared byte for byte on a 266k-die lot). On that lot, load-time analysis takes
308
+ 2.5 s in Chrome (was 3.7 s).
309
+ - **Stacked modes and mode switches in the gallery are faster.** Stacked cards key die positions by
310
+ number rather than by a string per die, and a map no longer rebuilds a key for every die on
311
+ each redraw unless dies are selected. On a 25-wafer, 266k-die lot in Chrome: switching to
312
+ stacked bins 2.0 s → 1.6 s, to value mode 1.3 s → 1.1 s.
313
+ - **The lot summary panel's test statistics are computed about three times faster.** The
314
+ quartiles of each test's pooled values are found by selection instead of sorting every value
315
+ (the same order statistics, so the same figures). On a 25-wafer, 266k-die lot the gallery
316
+ 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).
317
+ - **Insights opens several times faster on a large lot** (25 wafers, 266k dies: the Overview
318
+ 2.7 s → 0.65 s in Chrome, 0.9 s in WebKit). The Overview's Test Values table reuses the
319
+ pooled statistics the gallery's summary panel has already computed, whether or not the lot
320
+ has functional tests, and the pass-rate chart reads each die's values and recorded verdicts
321
+ in one pass per die.
322
+ Figures are unchanged.
323
+ - **`analyzeWaferMap` is about five times faster on large wafers** (a 25-wafer, 266k-die lot:
324
+ 32 s → 6.5 s in Chrome). Region membership, die keys and cluster neighbour lookups are each
325
+ computed once per analysis, not once per finding builder. Findings are unchanged.
326
+ - **Faster building and analysis in WebKit** (the desktop app on Linux and macOS, and Safari).
327
+ Checks that asked whether a die holds any test data read the die's keys one at a time and stop
328
+ at the first; the coverage count checks bins before test data; input checking decides each
329
+ test number's legality once per wafer instead of once per die. On a 266k-die lot in WebKitGTK,
330
+ building fell from 2.5 s to 1.1 s and analysis from 14 s to 7 s. Results are unchanged.
331
+ - **A hidden Summary panel is rendered when it is opened**, not on every mount and option
332
+ change. Every gallery card has one, so a large gallery mounts faster (266k-die lot: 7.3 s →
333
+ 5.0 s) and switches plot mode faster (3.4 s → 1.4 s).
334
+ - **Test values are held as one column per test**, not as an object on each die, so a map
335
+ holds a small fraction of the memory it did: on a 266k-die, 51-test lot in Chrome, about 330 MB
336
+ where it was 1.1 GB. A result from `createWafermapWorker` moves its columns to the page without
337
+ copying them.
338
+ - **Die outlines are drawn as merged lines**, one per run of shared edges instead of four sides
339
+ per die. Galleries draw 10–17% faster in WebKit (the desktop app on Linux), where stroking each
340
+ die was most of the drawing time. Every outline is now the same weight in every browser: Chrome
341
+ drew the edge two dies share slightly darker than the wafer's outer edge, WebKit did not.
342
+
343
+ ### Added
344
+
345
+ - **`HighlightWaferTarget.dieKeysByWafer`** — a lot regional finding names its region's dies
346
+ on each counted wafer, keyed by wafer index; the gallery highlights the region on every
347
+ counted card from it.
348
+
349
+ - **`results` can be columns (`DieColumns`).** A host that already holds its results as columns
350
+ (a parser, Arrow, Parquet) passes one array per field and, for test values and verdicts, the
351
+ indices of the records that have one with their values. The map is built with no object per
352
+ record or die. Retests, derived tests, input checks and warnings behave exactly as for rows.
353
+ Missing entries are `NaN` or STDF's missing values (−32768 for coordinates, 65535 for bins and
354
+ sites). Mismatched lengths or out-of-range and repeated indices throw.
355
+
356
+ ---
357
+
25
358
  ## [0.31.0] — 2026-09-25
26
359
 
27
360
  ### Breaking
@@ -1371,7 +1704,7 @@ export's notice names its replacement; the old names are still removed in 0.31.0
1371
1704
  placements take a fixed 180px band with `overflowY: hidden`, which most wafers' content
1372
1705
  exceeds, so the last visible row was cut mid-line with nothing indicating more existed. All
1373
1706
  placements now scroll, the way `'right'`/`'left'` already did. (Whether this content suits a
1374
- wide-short band at all is a separate open question — see TODO.md.)
1707
+ wide-short band at all is a separate open question.)
1375
1708
  - **Closing and reopening Insights no longer discards the shared axis toggles.** `axisPrefs`
1376
1709
  ("axis includes limits" / "clip outliers") lived inside `renderDistributionsSection`, which
1377
1710
  `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-~49%20kB-blue)
8
+ ![bundle](https://img.shields.io/badge/data%20layer%20min%2Bgz-~60%20kB-blue)
9
9
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
10
10
 
11
11
  <img src="docs/images/hero-test-values.png" alt="wafermap demo" style="max-width:640px; display:block; margin:8px 0;" />
@@ -13,7 +13,7 @@
13
13
  Browser-first wafer map visualization for semiconductor test data.
14
14
 
15
15
  **Zero runtime dependencies.** Pure ES modules with TypeScript types — works in React,
16
- Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~49 kB
16
+ Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~60 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,4 +1,5 @@
1
1
  import { type SaveImageHandler } from '../toolbar.js';
2
+ import { robustFence } from '../../stats/math.js';
2
3
  export type { SaveImageHandler };
3
4
  /**
4
5
  * The card's own expand button is hidden for the modal's duration and shown
@@ -571,18 +572,7 @@ export declare function fillTextOnHalo(ctx: CanvasRenderingContext2D, text: stri
571
572
  export declare function strokeLimitLine(ctx: CanvasRenderingContext2D, line: LimitLine, color: string, x0: number, y0: number, x1: number, y1: number): void;
572
573
  /** The "Limits:" dropdown, offered only where a test has both kinds. */
573
574
  export declare function makeLimitsSelect(current: LimitsShown, onChange: (v: LimitsShown) => void, ownerDocument?: Document): HTMLLabelElement;
574
- /** Robust outlier fence over a value list: Tukey's `Q1 − k·IQR … Q3 + k·IQR`.
575
- *
576
- * Deliberately NOT mean ± 3σ. σ is computed FROM the data including the
577
- * outlier, so a single reading of 1e30 inflates σ far enough that the fence no
578
- * longer excludes it — the classic masking failure, and it fails hardest exactly
579
- * when the outlier is worst. Quartiles are unmoved by the extreme tail.
580
- *
581
- * Returns null when there are too few values for quartiles to mean anything. */
582
- export declare function robustFence(values: number[], k?: number): {
583
- lo: number;
584
- hi: number;
585
- } | null;
575
+ export { robustFence };
586
576
  /**
587
577
  * Whether a panel should include the spec limits in its axis BY DEFAULT.
588
578
  *
@@ -1 +1 @@
1
- import{SHADOW as D,LEADING as j,wireControlHover as L,controlStyle as I,SPACE as m,RADIUS as C,fontPx as k,FONT as S,CLR as f,Z_BASE as X,menuLayerFor as K,wireListNavigation as Y,MENU_SEARCH_THRESHOLD as W,makeMenuSearchBox as Z,markMenuTrigger as R,saveImageBlob as J,openReparentedModal as Q}from"../toolbar.js";import{ICONS as _}from"../icons.js";import{minOf as ee,maxOf as te}from"../../core/utils.js";import{fmt as ne,fmtColorbarAxis as oe}from"../../renderer/fmt.js";import{markedTestLabel as ie,DERIVED_LANE_PAD as re}from"../../renderer/testLabel.js";export function openChartExpandModal(e,o,n={}){for(const s of e.querySelectorAll('div[style*="position: absolute"]'))s.style.display==="block"&&s.style.pointerEvents==="none"&&(s.style.display="none");const t=e.querySelector("[data-wmap-chart-expand]"),i=e.getAttribute("style")??"",r=e.dataset.wmapChartGrow??"plot";Object.assign(e.style,{flex:"1",minHeight:"0",alignSelf:"stretch",border:"none",borderRadius:"0"}),e.dataset.wmapExpanded="1";const l=Q([e],{title:o,anchor:n.anchor,ownerDocument:e.ownerDocument,boxSize:le[r],onClosed:()=>{delete e.dataset.wmapExpanded,e.setAttribute("style",i),t&&(t.style.display="flex"),n.onClosed?.()}});if(!l){delete e.dataset.wmapExpanded,e.setAttribute("style",i);return}if(t&&(t.style.display="none"),r==="rows"){const s=()=>{const a=Number(e.dataset.wmapChartFullH);if(!a)return;const d=l.box.offsetHeight-l.contentWrap.clientHeight;l.setNaturalHeight(`min(90vh, ${Math.ceil(d+a)}px)`)};s(),(e.ownerDocument.defaultView??window).requestAnimationFrame(s)}}const le={plot:{width:"min(94vw, 1100px)",height:"min(90vh, 760px)"},rows:{width:"min(94vw, 960px)",height:"min(90vh, 700px)"},square:{width:"min(94vw, 820px)",height:"min(90vh, 860px)"}};export const GRID_ALPHA=.4;export function withAlpha(e,o){const n=e.trim();let t,i,r,l=1;const s=n.match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i),a=n.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i);if(s){const d=s[1].length===3?s[1].split("").map(c=>c+c).join(""):s[1];[t,i,r]=[0,2,4].map(c=>parseInt(d.slice(c,c+2),16)),d.length===8&&(l=parseInt(d.slice(6,8),16)/255)}else if(a)[t,i,r]=[a[1],a[2],a[3]].map(Number),a[4]!==void 0&&(l=a[4].endsWith("%")?parseFloat(a[4])/100:Number(a[4]));else return e;return`rgba(${t}, ${i}, ${r}, ${+(l*o).toFixed(3)})`}const se={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040",accent:"#1a66cc",limitLine:"#b45309"},P={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border",accent:"icon-active",limitLine:"warn-text"};export function resolveChartCanvasColors(e){const o=getComputedStyle(e),n={};for(const t of Object.keys(P)){const i=o.getPropertyValue(`--wmap-${P[t]}`).trim();n[t]=i||se[t]}return n.grid=withAlpha(n.border,GRID_ALPHA),n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,o){const n=e.ownerDocument.defaultView??window;let t=0;const i=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,o()}))});return i.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,i.disconnect()}}}export function growCardToFitContent(e,o,n){const t=`${M(e,o)+n}px`;e.style.minHeight!==t&&(e.style.minHeight=t)}function M(e,o){const n=e.ownerDocument.defaultView??window,t=n.getComputedStyle(e);let i=0;for(const r of Array.from(e.children)){if(r===o)continue;const l=r,s=n.getComputedStyle(l);s.display==="none"||s.position==="absolute"||s.position==="fixed"||(i+=l.offsetHeight+(parseFloat(s.marginTop)||0)+(parseFloat(s.marginBottom)||0))}return t.boxSizing==="border-box"&&(i+=(parseFloat(t.paddingTop)||0)+(parseFloat(t.paddingBottom)||0)+(parseFloat(t.borderTopWidth)||0)+(parseFloat(t.borderBottomWidth)||0)),i}function z(e,o){const n=e.ownerDocument.defaultView??window;let t=0;for(const i of Array.from(e.children)){if(i===o)continue;const r=i,l=n.getComputedStyle(r);l.display==="none"||l.position==="absolute"||l.position==="fixed"||(t+=r.offsetHeight+(parseFloat(l.marginTop)||0)+(parseFloat(l.marginBottom)||0))}return t}export const WAFER_MENU_HINT="<br><em>right-click to chart this wafer</em>";export function populationPhrase(e,o,n){const t=e.toLocaleString(),i=e===1?"die":"dies";return n?e===o?`${t} ${i} ${n}`:`${t} of ${o.toLocaleString()} dies ${n} (partial and edge-excluded dies left out)`:`${t} ${i}`}export function setChartGrow(e,o){e.dataset.wmapChartGrow=o}export function isExpandedCard(e){return e.dataset.wmapExpanded==="1"}export function bodyRoom(e,o,n){const i=(e.ownerDocument.defaultView??window).getComputedStyle(e),r=e.clientHeight-(parseFloat(i.paddingTop)||0)-(parseFloat(i.paddingBottom)||0),l=i.boxSizing==="border-box"?(parseFloat(i.paddingTop)||0)+(parseFloat(i.paddingBottom)||0)+(parseFloat(i.borderTopWidth)||0)+(parseFloat(i.borderBottomWidth)||0):0;return r-(M(e,o)-l)-(n?z(o,n):0)}export function fitRowsHeight(e,o,n,t,i=0){if(growCardToFitContent(e,o,i+n),e.dataset.wmapChartFullH=String(Math.ceil(M(e,o)+i+t)),!isExpandedCard(e))return n;const r=bodyRoom(e,o)-i;return Math.max(n,Math.min(t,Math.floor(r)))}export function chartFillHeight(e,o,n,t){const i=z(o,n);return growCardToFitContent(e,o,i+t),Math.max(t,o.clientHeight-i)}export function chartDpr(e){return(e.ownerDocument.defaultView??window).devicePixelRatio||1}export function prepareCanvas(e,o,n,t){const i=chartDpr(e);e.width=Math.max(1,Math.floor(n*i)),e.height=Math.max(1,Math.floor(t*i)),e.style.width=`${n}px`,e.style.height=`${t}px`;const r=e.getContext("2d");return r?(r.setTransform(i,0,0,i,0,0),r.clearRect(0,0,n,t),r.font=`${k(-1)}px system-ui, sans-serif`,r.textBaseline="middle",{ctx:r,theme:resolveChartCanvasColors(o)}):null}export function applyCanvasFlow(e,o=0){let n;if(typeof o=="number")n=o;else{const t=(o.ownerDocument.defaultView??window).getComputedStyle(o);n=o.offsetTop+o.offsetHeight+(parseFloat(t.marginBottom)||0)}e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${n}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:ne(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,o,n){if(!o&&n===void 0)return{tick:formatValue,tickWithUnit:formatValue,unitLabel:""};const{tickFmt:t,axisLabel:i,scaledUnit:r}=oe(e,null,o,"engineering",n);return{tick:t,tickWithUnit:r?l=>`${t(l)} ${r}`:t,unitLabel:i}}const ae=8;export const VERTICAL_TICK_SPACING_PX=28;export function horizontalTickSpacing(e,o,n){return(t,i)=>{const r=makeAxisFormat(o,n,t).tick;return i.reduce((l,s)=>Math.max(l,e.measureText(r(s)).width),0)+ae}}export function drawAxisUnit(e,o,n,t,i){const r={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font=`${k(-1)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="middle",e.fillStyle=i,e.fillText(`(${o})`,n,t),Object.assign(e,r)}export function saveCanvasPng(e,o,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const i=t.getContext("2d");i.fillStyle="#ffffff",i.fillRect(0,0,t.width,t.height),i.drawImage(e,0,0),t.toBlob(r=>{r&&J(r,o,n)},"image/png")}const V="10px";export function chartSwatchCss(e,o="solid"){return["display:inline-block",`width:${V}`,`height:${V}`,`border-radius:${C.pill}`,`background:${o==="outline"?"transparent":e}`,`border:1px ${o==="outline"?"dashed":"solid"} ${e}`,"box-sizing:border-box","flex-shrink:0"].join(";")}const ce="0.35";export function makeSeriesLegendItem(e,o,n=document){const t=n.createElement("button");t.type="button",Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:m.xs,padding:`${m.xs} ${m.md}`,borderRadius:C.pill,border:`1px solid ${f.menuBorder}`,background:"none",cursor:"pointer",fontSize:S.body,color:f.text,whiteSpace:"nowrap"}),L(t,"bare");const i=n.createElement("span");return i.style.cssText=chartSwatchCss(o),t.append(i,n.createTextNode(e)),{el:t,setState({selected:r,dimmed:l}){t.style.borderColor=r?f.iconActive:f.menuBorder,t.style.fontWeight=r?"600":"400",t.style.opacity=l?ce:"1",t.setAttribute("aria-pressed",r?"true":"false")}}}export function cardFrameStyle(){return{background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:C.container,padding:m.xl,fontVariantNumeric:"tabular-nums"}}export function cardShell(e,o,n=document){const t=n.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",...cardFrameStyle(),minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const i=t.ownerDocument.createElement("div");Object.assign(i.style,{display:"flex",alignItems:"center",gap:m.sm,paddingBottom:"6px",marginBottom:m.md,borderBottom:`1px solid ${f.menuBorder}`});const r=t.ownerDocument.createElement("div");r.textContent=e,Object.assign(r.style,{color:f.value,fontSize:S.heading,fontWeight:"600",flex:"1"}),i.appendChild(r);const l=makeTooltip(t),s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Save as PNG"),s.innerHTML=_.download,Object.assign(s.style,{...I("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:j.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(s),attachChartTip(s,t,l,"Save as PNG"),s.addEventListener("click",()=>{const u=t.querySelector("canvas");u&&saveCanvasPng(u,e,o)}),i.appendChild(s);const a=t.ownerDocument.createElement("button");a.type="button",a.setAttribute("aria-label","Expand"),a.innerHTML=_.expand,Object.assign(a.style,{...I("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:j.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(a),attachChartTip(a,t,l,"Expand"),a.dataset.wmapChartExpand="1",a.addEventListener("click",()=>openChartExpandModal(t,r.textContent??e)),i.appendChild(a),t.appendChild(i);const d=t.ownerDocument.createElement("div");Object.assign(d.style,{display:"flex",gap:m.sm,marginBottom:m.md,flexWrap:"wrap",alignItems:"center"}),t.appendChild(d);const c=t.ownerDocument.createElement("div");return Object.assign(c.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(c),{card:t,heading:r,controlsRow:d,body:c}}export function makeSegmented(e,o,n,t=document,i=!1){const r=t.createElement("div");r.setAttribute("role","radiogroup"),Object.assign(r.style,{display:"inline-flex",border:`1px solid ${f.menuBorder}`,borderRadius:C.control,overflow:"hidden"});const l=`seg-${Math.random().toString(36).slice(2,9)}`,s=[];return e.forEach(([a,d],c)=>{const u=t.createElement("label");Object.assign(u.style,{display:"inline-flex",alignItems:"center",fontSize:S.body,padding:i?"1px 6px":"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:c>0?`1px solid ${f.menuBorder}`:"none"}),L(u,"bare");const p=t.createElement("input");p.type="radio",p.name=l,p.value=a,p.checked=a===o,Object.assign(p.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const w=()=>{u.style.background=p.checked?f.bgActive:f.menuBg,u.style.color=p.checked?f.iconActive:f.text,u.dataset.on=p.checked?"true":"false"};s.push(w),w(),p.addEventListener("change",()=>{if(p.checked){for(const v of s)v();n(a)}}),u.append(p,t.createTextNode(d)),r.appendChild(u)}),r}export function makeBackButton(e,o=document){const n=o.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{...I("outlined"),background:f.menuBg,color:f.label}),L(n),n.addEventListener("click",e),n}export function renderEmptyState(e,o,n){const t=e.ownerDocument.createElement("div");t.textContent=o,Object.assign(t.style,{color:f.label,fontSize:S.body,padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeListSelect(e,o,n,t={}){const{maxWidth:i="200px",ownerDocument:r=document,ariaLabel:l,emptyText:s,searchPlaceholder:a="Filter\u2026",hook:d}=t;let c=o;const u=x=>e.find(O=>O.value===x)?.label??"",p=r.createElement("button");p.type="button",d&&(p.dataset.wmapSelect=d),l&&p.setAttribute("aria-label",l),Object.assign(p.style,{fontSize:S.body,padding:`${m.xxs} ${m.sm}`,background:f.menuBg,color:f.text,border:`1px solid ${f.menuBorder}`,borderRadius:C.control,maxWidth:i,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:m.sm,justifyContent:"space-between"}),L(p),R(p,!1);const w=r.createElement("span");Object.assign(w.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const v=r.createElement("span");v.textContent="\u25BE",v.style.flex="0 0 auto",p.append(w,v);const E=()=>{w.textContent=e.length===0?s??"(none)":u(c)||s||"(none)"};if(E(),e.length===0)return p.disabled=!0,p.style.opacity="0.6",p.style.cursor="default",Object.defineProperty(p,"value",{get:()=>c,set:x=>{c=x,E()}});let b=null;const B=()=>{b&&(b.remove(),b=null,R(p,!1))},$=()=>{B(),p.focus()};function G(){const x=p.getBoundingClientRect(),O=r.defaultView??window,N=Math.max(x.width,220),q=Math.min(x.left,Math.max(4,(O.innerWidth??1/0)-N-4));b=r.createElement("div"),b.setAttribute("role","listbox"),b.setAttribute("aria-label",l??"Options"),Object.assign(b.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${q}px`,background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:C.control,boxShadow:D.menu,zIndex:X,minWidth:`${N}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const T=[],F=()=>T.filter(h=>h.row.style.display!=="none").map(h=>h.row);if(e.length>W){const h=Z(y=>{for(const g of T)g.row.style.display=g.label.includes(y)?"":"none"},a,r);Object.assign(h.style,{position:"sticky",top:"0",zIndex:"1",background:f.menuBg}),h.addEventListener("keydown",y=>{if(y.key==="Escape"){$();return}y.key==="ArrowDown"&&(y.preventDefault(),F()[0]?.focus())}),b.appendChild(h)}for(const h of e){const y=h.value===c,g=r.createElement("div");g.textContent=h.label,g.setAttribute("role","option"),g.setAttribute("aria-selected",y?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:`${m.sm} ${m.xl}`,fontSize:S.body,cursor:"pointer",color:y?f.iconActive:f.text,fontWeight:y?"700":"400",background:y?f.menuActive:"transparent",whiteSpace:"nowrap"}),g.addEventListener("mouseenter",()=>{y||(g.style.background=f.menuHover)}),g.addEventListener("mouseleave",()=>{y||(g.style.background="transparent")}),g.addEventListener("click",U=>{U.stopPropagation(),c=h.value,E(),$(),n(h.value)}),b.appendChild(g),T.push({row:g,label:h.label.toLowerCase()})}if(Y(b,F,$),K(p).appendChild(b),R(p,!0),e.length<=W){const h=e.findIndex(y=>y.value===c);F()[h>=0?h:0]?.focus()}}p.addEventListener("click",x=>{if(x.stopPropagation(),b){B();return}G()});const H=x=>{if(!p.isConnected){r.removeEventListener("click",H);return}b&&!b.contains(x.target)&&!p.contains(x.target)&&B()};return r.addEventListener("click",H),Object.defineProperty(p,"value",{get:()=>c,set:x=>{c=x,E()}})}export function testOptionLabels(e){const o=e.some(n=>n.derived);return e.map(n=>(o&&!n.derived?re:"")+ie(n,n.testNumber))}export function makeTestSelect(e,o,n,t={}){const{maxWidth:i="200px",emptyText:r="No parametric tests",ownerDocument:l=document}=t;return makeListSelect(testOptionLabels(e).map((s,a)=>({value:String(e[a].testNumber),label:s})),o!==null?String(o):"",s=>n(Number(s)),{maxWidth:i,ownerDocument:l,ariaLabel:"Test",emptyText:r,searchPlaceholder:"Filter tests\u2026"})}export function makeLinkedAxisPrefs(e,o,n,t=document){let i=o?.includeLimits,r=o?.clipOutliers??!1,l=o?.limits??"both";const s=()=>n({includeLimits:i,clipOutliers:r,limits:l});return{get:()=>({includeLimits:i,clipOutliers:r,limits:l}),sync(a,d,c=!1){e.innerHTML="",c&&e.appendChild(makeLimitsSelect(l,u=>{l=u,s()},t)),d&&e.appendChild(makeToggle("Axis includes limits",a,u=>{i=u,s()},t)),e.appendChild(makeToggle("Clip outliers",r,u=>{r=u,s()},t))},set(a){const d=a.limits??"both";return a.includeLimits===i&&a.clipOutliers===r&&d===l?!1:(i=a.includeLimits,r=a.clipOutliers,l=d,!0)}}}export function makeLinkedTestSelect(e,o,n,t={}){let i=o;const r=makeTestSelect(e,i,l=>{l!==i&&(i=l,n(l))},t);return{el:r,get:()=>i,set(l){return i===l||!e.some(s=>s.testNumber===l)?!1:(i=l,r.value=String(l),!0)}}}export const ALL_GROUPS="\0all";export function makeLinkedGroupSelect(e,o,n,t,i={}){let r=n;const l=c=>c??ALL_GROUPS,s=c=>c===ALL_GROUPS?null:c,a=makeLabeledSelect(o,[{value:ALL_GROUPS,label:"All groups"},...e.map(c=>({value:c,label:c}))],l(r),c=>{const u=s(c);u!==r&&(r=u,t(u))},{...i,hook:"group-scope"}),d=a.querySelector('[data-wmap-select="group-scope"]');return{el:a,get:()=>r,set(c){return r===c||c!==null&&!e.includes(c)?!1:(r=c,d&&(d.value=l(c)),!0)}}}export function makeWaferSelect(e,o,n,t={}){const{maxWidth:i="160px",allLabel:r="All wafers",ownerDocument:l=document}=t,s="\0all";return makeListSelect([{value:s,label:r},...e.map((a,d)=>({value:String(d),label:a.label??`#${d}`}))],o===null?s:String(o),a=>n(a===s?null:Number(a)),{maxWidth:i,ownerDocument:l,ariaLabel:r,searchPlaceholder:"Filter wafers\u2026"})}export function makeToggle(e,o,n,t=document){const i=t.createElement("label");Object.assign(i.style,{display:"inline-flex",alignItems:"center",gap:m.xs,fontSize:S.body,color:f.label,cursor:"pointer",userSelect:"none"}),L(i,"bare");const r=t.createElement("input");return r.type="checkbox",r.checked=o,r.style.cssText="margin:0;cursor:pointer;",r.addEventListener("change",()=>n(r.checked)),i.append(r,t.createTextNode(e)),i}export function makeLabeledSelect(e,o,n,t,i={}){const{maxWidth:r="160px",hook:l,ownerDocument:s=document}=i,a=s.createElement("label");a.textContent=e,Object.assign(a.style,{color:f.label,fontSize:S.body,display:"flex",alignItems:"center",gap:m.xs});const d=makeListSelect(o,n,t,{maxWidth:r,ownerDocument:s,hook:l,ariaLabel:e.replace(/:\s*$/,"")});return a.appendChild(d),a}export function makeChartGridWrap(e=document){const o=e.createElement("div");return o.dataset.wmapChartGrid="1",Object.assign(o.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:m.lg,flex:"0 0 auto"}),o}export function makeTooltip(e){const o=e.ownerDocument.createElement("div");return Object.assign(o.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:"rgba(30, 32, 40, 0.93)",color:"#f0f0f2",border:"1px solid rgba(255,255,255,0.10)",borderRadius:C.control,padding:`${m.sm} ${m.xl}`,fontSize:S.sub,fontFamily:S.family,maxWidth:"320px",whiteSpace:"normal",overflowWrap:"break-word",boxShadow:D.menu}),e.appendChild(o),o}export function attachChartTip(e,o,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",i=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,o,i.clientX,i.clientY)}),e.addEventListener("mousemove",i=>{n.style.display==="block"&&positionChartTooltip(n,o,i.clientX,i.clientY)}),e.addEventListener("mouseleave",()=>{n.style.display="none"}),e.addEventListener("click",()=>{n.style.display="none"}),e.addEventListener("focus",()=>{n.textContent=t,n.style.display="block";const i=e.getBoundingClientRect();positionChartTooltip(n,o,i.left+i.width/2,i.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,o,n,t){const i=o.getBoundingClientRect(),r=14,l=4;e.style.left="0",e.style.top="0";const s=e.offsetWidth,a=e.offsetHeight;let d=n-i.left+r,c=t-i.top+r;d+s+l>i.width&&(d=n-i.left-r-s),c+a+l>i.height&&(c=t-i.top-r-a),d=Math.max(l,d),c=Math.max(l,c),e.style.left=`${d}px`,e.style.top=`${c}px`}export const LIMIT_LABEL={test:{lo:"Lo limit",hi:"Hi limit"},spec:{lo:"LSL",hi:"USL"}},LIMIT_DASH={test:[3,3],spec:[10,4]},LIMITS_SHOWN_OPTIONS=[{value:"both",label:"Test + spec"},{value:"test",label:"Test limits"},{value:"spec",label:"Spec limits (LSL/USL)"},{value:"none",label:"None"}];function A(e,o){const[n,t]=o==="test"?[e?.limitLow,e?.limitHigh]:[e?.specLow,e?.specHigh],i=[];return n!==void 0&&Number.isFinite(n)&&i.push({value:n,kind:o,end:"lo",label:LIMIT_LABEL[o].lo}),t!==void 0&&Number.isFinite(t)&&i.push({value:t,kind:o,end:"hi",label:LIMIT_LABEL[o].hi}),i}export function hasBothLimitKinds(e){return A(e,"test").length>0&&A(e,"spec").length>0}export function limitLines(e,o="both"){if(o==="none")return[];const n=A(e,"test"),t=A(e,"spec"),i=o==="test"?n:o==="spec"?t:[...n,...t];return i.length?i:[...n,...t]}export function limitExtent(e){if(!e.length)return{};const o=e.map(n=>n.value);return{lo:ee(o),hi:te(o)}}export function stackLabelRows(e,o=4){const n=e.map((r,l)=>({...r,i:l})).sort((r,l)=>r.start-l.start),t=[],i=new Array(e.length).fill(0);for(const r of n){let l=t.findIndex(s=>r.start>=s+o);l===-1?(l=t.length,t.push(r.end)):t[l]=r.end,i[r.i]=l}return i}export function fillTextOnHalo(e,o,n,t,i){if(i){const r=e.measureText(o),l=2,s=e.fillStyle;e.fillStyle=i,e.fillRect(n-r.actualBoundingBoxLeft-l,t-r.actualBoundingBoxAscent-l,r.actualBoundingBoxLeft+r.actualBoundingBoxRight+2*l,r.actualBoundingBoxAscent+r.actualBoundingBoxDescent+2*l),e.fillStyle=s}e.fillText(o,n,t)}export function strokeLimitLine(e,o,n,t,i,r,l){e.save(),e.strokeStyle=n,e.lineWidth=1,e.setLineDash(LIMIT_DASH[o.kind]),e.beginPath(),e.moveTo(t,i),e.lineTo(r,l),e.stroke(),e.restore()}export function makeLimitsSelect(e,o,n=document){return makeLabeledSelect("Limits:",LIMITS_SHOWN_OPTIONS,e,t=>o(t),{maxWidth:"170px",hook:"limits-shown",ownerDocument:n})}export function robustFence(e,o=1.5){const n=Float64Array.from(e.filter(s=>Number.isFinite(s)));if(n.sort(),n.length<8)return null;const t=s=>{const a=(n.length-1)*s,d=Math.floor(a),c=Math.ceil(a);return d===c?n[d]:n[d]+(n[c]-n[d])*(a-d)},i=t(.25),r=t(.75),l=r-i;return l===0?null:{lo:i-o*l,hi:r+o*l}}export function shouldIncludeLimitsByDefault(e,o,n,t,i=1/3){if(n===void 0&&t===void 0||!Number.isFinite(e)||!Number.isFinite(o))return!1;const r=Math.min(e,n??e),s=Math.max(o,t??o)-r;if(s<=0)return!0;const a=o-e;return a===0?!0:a/s>=i}export function resolveAxisRange(e){const{dataMin:o,dataMax:n,limits:t=[],includeLimits:i,clipOutliers:r,values:l}=e;let s=o,a=n,d=0;if(r&&l?.length){const u=robustFence(l);if(u){const p=Math.max(s,u.lo),w=Math.min(a,u.hi);w>p&&(d=l.filter(v=>Number.isFinite(v)&&(v<p||v>w)).length,s=p,a=w)}}if(i)for(const u of t)s=Math.min(s,u.value),a=Math.max(a,u.value);s===a&&(s-=1,a+=1);const c=[];for(const u of t)u.value<s?c.push({value:u.value,label:u.label,side:"lo"}):u.value>a&&c.push({value:u.value,label:u.label,side:"hi"});return{lo:s,hi:a,offAxis:c,clippedCount:d}}export function limitLabelSide(e,o,n,t,i,r=3){const l=i?-1:1;return(l<0?e-r-o>=n:e+r+o<=t)?l:-l}export function drawOffAxisLimits(e,o,n,t,i,r,l){if(!o.length)return;e.save(),e.fillStyle=i,e.font=`${k(-1)}px system-ui, sans-serif`;const s={lo:0,hi:0},a=k(-1)+2;for(const{value:d,label:c,side:u}of o){const p=s[u]++*a,w=t==="horizontal"?u==="lo"?"\u2190":"\u2192":u==="lo"?"\u2193":"\u2191",v=u==="lo"?`${w} ${c} ${r(d)}`:`${c} ${r(d)} ${w}`;t==="horizontal"?(e.textBaseline="top",e.textAlign=u==="lo"?"left":"right",fillTextOnHalo(e,v,u==="lo"?n.left+2:n.right-2,n.top+2+p,l)):(e.textBaseline=u==="lo"?"bottom":"top",e.textAlign="left",fillTextOnHalo(e,v,n.left+4,u==="lo"?n.bottom-2-p:n.top+2+p,l))}e.restore()}
1
+ import{SHADOW as N,LEADING as j,wireControlHover as L,controlStyle as F,SPACE as m,RADIUS as C,fontPx as k,FONT as S,CLR as f,Z_BASE as K,menuLayerFor as Y,wireListNavigation as Z,MENU_SEARCH_THRESHOLD as W,makeMenuSearchBox as J,markMenuTrigger as R,saveImageBlob as Q,openReparentedModal as ee}from"../toolbar.js";import{ICONS as _}from"../icons.js";import{minOf as te,maxOf as ne}from"../../core/utils.js";import{robustFence as P}from"../../stats/math.js";import{fmt as oe,fmtColorbarAxis as ie}from"../../renderer/fmt.js";import{markedTestLabel as re,DERIVED_LANE_PAD as le}from"../../renderer/testLabel.js";export function openChartExpandModal(e,o,n={}){for(const s of e.querySelectorAll('div[style*="position: absolute"]'))s.style.display==="block"&&s.style.pointerEvents==="none"&&(s.style.display="none");const t=e.querySelector("[data-wmap-chart-expand]"),i=e.getAttribute("style")??"",r=e.dataset.wmapChartGrow??"plot";Object.assign(e.style,{flex:"1",minHeight:"0",alignSelf:"stretch",border:"none",borderRadius:"0"}),e.dataset.wmapExpanded="1";const l=ee([e],{title:o,anchor:n.anchor,ownerDocument:e.ownerDocument,boxSize:se[r],onClosed:()=>{delete e.dataset.wmapExpanded,e.setAttribute("style",i),t&&(t.style.display="flex"),n.onClosed?.()}});if(!l){delete e.dataset.wmapExpanded,e.setAttribute("style",i);return}if(t&&(t.style.display="none"),r==="rows"){const s=()=>{const a=Number(e.dataset.wmapChartFullH);if(!a)return;const p=l.box.offsetHeight-l.contentWrap.clientHeight;l.setNaturalHeight(`min(90vh, ${Math.ceil(p+a)}px)`)};s(),(e.ownerDocument.defaultView??window).requestAnimationFrame(s)}}const se={plot:{width:"min(94vw, 1100px)",height:"min(90vh, 760px)"},rows:{width:"min(94vw, 960px)",height:"min(90vh, 700px)"},square:{width:"min(94vw, 820px)",height:"min(90vh, 860px)"}};export const GRID_ALPHA=.4;export function withAlpha(e,o){const n=e.trim();let t,i,r,l=1;const s=n.match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i),a=n.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i);if(s){const p=s[1].length===3?s[1].split("").map(c=>c+c).join(""):s[1];[t,i,r]=[0,2,4].map(c=>parseInt(p.slice(c,c+2),16)),p.length===8&&(l=parseInt(p.slice(6,8),16)/255)}else if(a)[t,i,r]=[a[1],a[2],a[3]].map(Number),a[4]!==void 0&&(l=a[4].endsWith("%")?parseFloat(a[4])/100:Number(a[4]));else return e;return`rgba(${t}, ${i}, ${r}, ${+(l*o).toFixed(3)})`}const ae={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040",accent:"#1a66cc",limitLine:"#b45309"},z={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border",accent:"icon-active",limitLine:"warn-text"};export function resolveChartCanvasColors(e){const o=getComputedStyle(e),n={};for(const t of Object.keys(z)){const i=o.getPropertyValue(`--wmap-${z[t]}`).trim();n[t]=i||ae[t]}return n.grid=withAlpha(n.border,GRID_ALPHA),n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,o){const n=e.ownerDocument.defaultView??window;let t=0;const i=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,o()}))});return i.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,i.disconnect()}}}export function growCardToFitContent(e,o,n){const t=`${H(e,o)+n}px`;e.style.minHeight!==t&&(e.style.minHeight=t)}function H(e,o){const n=e.ownerDocument.defaultView??window,t=n.getComputedStyle(e);let i=0;for(const r of Array.from(e.children)){if(r===o)continue;const l=r,s=n.getComputedStyle(l);s.display==="none"||s.position==="absolute"||s.position==="fixed"||(i+=l.offsetHeight+(parseFloat(s.marginTop)||0)+(parseFloat(s.marginBottom)||0))}return t.boxSizing==="border-box"&&(i+=(parseFloat(t.paddingTop)||0)+(parseFloat(t.paddingBottom)||0)+(parseFloat(t.borderTopWidth)||0)+(parseFloat(t.borderBottomWidth)||0)),i}function V(e,o){const n=e.ownerDocument.defaultView??window;let t=0;for(const i of Array.from(e.children)){if(i===o)continue;const r=i,l=n.getComputedStyle(r);l.display==="none"||l.position==="absolute"||l.position==="fixed"||(t+=r.offsetHeight+(parseFloat(l.marginTop)||0)+(parseFloat(l.marginBottom)||0))}return t}export const WAFER_MENU_HINT="<br><em>right-click to chart this wafer</em>";export function populationPhrase(e,o,n){const t=e.toLocaleString(),i=e===1?"die":"dies";return n?e===o?`${t} ${i} ${n}`:`${t} of ${o.toLocaleString()} dies ${n} (partial and edge-excluded dies left out)`:`${t} ${i}`}export function setChartGrow(e,o){e.dataset.wmapChartGrow=o}export function isExpandedCard(e){return e.dataset.wmapExpanded==="1"}export function bodyRoom(e,o,n){const i=(e.ownerDocument.defaultView??window).getComputedStyle(e),r=e.clientHeight-(parseFloat(i.paddingTop)||0)-(parseFloat(i.paddingBottom)||0),l=i.boxSizing==="border-box"?(parseFloat(i.paddingTop)||0)+(parseFloat(i.paddingBottom)||0)+(parseFloat(i.borderTopWidth)||0)+(parseFloat(i.borderBottomWidth)||0):0;return r-(H(e,o)-l)-(n?V(o,n):0)}export function fitRowsHeight(e,o,n,t,i=0){if(growCardToFitContent(e,o,i+n),e.dataset.wmapChartFullH=String(Math.ceil(H(e,o)+i+t)),!isExpandedCard(e))return n;const r=bodyRoom(e,o)-i;return Math.max(n,Math.min(t,Math.floor(r)))}export function chartFillHeight(e,o,n,t){const i=V(o,n);return growCardToFitContent(e,o,i+t),Math.max(t,o.clientHeight-i)}export function chartDpr(e){return(e.ownerDocument.defaultView??window).devicePixelRatio||1}export function prepareCanvas(e,o,n,t){const i=chartDpr(e);e.width=Math.max(1,Math.floor(n*i)),e.height=Math.max(1,Math.floor(t*i)),e.style.width=`${n}px`,e.style.height=`${t}px`;const r=e.getContext("2d");return r?(r.setTransform(i,0,0,i,0,0),r.clearRect(0,0,n,t),r.font=`${k(-1)}px system-ui, sans-serif`,r.textBaseline="middle",{ctx:r,theme:resolveChartCanvasColors(o)}):null}export function applyCanvasFlow(e,o=0){let n;if(typeof o=="number")n=o;else{const t=(o.ownerDocument.defaultView??window).getComputedStyle(o);n=o.offsetTop+o.offsetHeight+(parseFloat(t.marginBottom)||0)}e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${n}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:oe(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,o,n){if(!o&&n===void 0)return{tick:formatValue,tickWithUnit:formatValue,unitLabel:""};const{tickFmt:t,axisLabel:i,scaledUnit:r}=ie(e,null,o,"engineering",n);return{tick:t,tickWithUnit:r?l=>`${t(l)} ${r}`:t,unitLabel:i}}const ce=8;export const VERTICAL_TICK_SPACING_PX=28;export function horizontalTickSpacing(e,o,n){return(t,i)=>{const r=makeAxisFormat(o,n,t).tick;return i.reduce((l,s)=>Math.max(l,e.measureText(r(s)).width),0)+ce}}export function drawAxisUnit(e,o,n,t,i){const r={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font=`${k(-1)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="middle",e.fillStyle=i,e.fillText(`(${o})`,n,t),Object.assign(e,r)}export function saveCanvasPng(e,o,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const i=t.getContext("2d");i.fillStyle="#ffffff",i.fillRect(0,0,t.width,t.height),i.drawImage(e,0,0),t.toBlob(r=>{r&&Q(r,o,n)},"image/png")}const G="10px";export function chartSwatchCss(e,o="solid"){return["display:inline-block",`width:${G}`,`height:${G}`,`border-radius:${C.pill}`,`background:${o==="outline"?"transparent":e}`,`border:1px ${o==="outline"?"dashed":"solid"} ${e}`,"box-sizing:border-box","flex-shrink:0"].join(";")}const de="0.35";export function makeSeriesLegendItem(e,o,n=document){const t=n.createElement("button");t.type="button",Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:m.xs,padding:`${m.xs} ${m.md}`,borderRadius:C.pill,border:`1px solid ${f.menuBorder}`,background:"none",cursor:"pointer",fontSize:S.body,color:f.text,whiteSpace:"nowrap"}),L(t,"bare");const i=n.createElement("span");return i.style.cssText=chartSwatchCss(o),t.append(i,n.createTextNode(e)),{el:t,setState({selected:r,dimmed:l}){t.style.borderColor=r?f.iconActive:f.menuBorder,t.style.fontWeight=r?"600":"400",t.style.opacity=l?de:"1",t.setAttribute("aria-pressed",r?"true":"false")}}}export function cardFrameStyle(){return{background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:C.container,padding:m.xl,fontVariantNumeric:"tabular-nums"}}export function cardShell(e,o,n=document){const t=n.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",...cardFrameStyle(),minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const i=t.ownerDocument.createElement("div");Object.assign(i.style,{display:"flex",alignItems:"center",gap:m.sm,paddingBottom:"6px",marginBottom:m.md,borderBottom:`1px solid ${f.menuBorder}`});const r=t.ownerDocument.createElement("div");r.textContent=e,Object.assign(r.style,{color:f.value,fontSize:S.heading,fontWeight:"600",flex:"1"}),i.appendChild(r);const l=makeTooltip(t),s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Save as PNG"),s.innerHTML=_.download,Object.assign(s.style,{...F("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:j.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(s),attachChartTip(s,t,l,"Save as PNG"),s.addEventListener("click",()=>{const d=t.querySelector("canvas");d&&saveCanvasPng(d,e,o)}),i.appendChild(s);const a=t.ownerDocument.createElement("button");a.type="button",a.setAttribute("aria-label","Expand"),a.innerHTML=_.expand,Object.assign(a.style,{...F("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:j.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(a),attachChartTip(a,t,l,"Expand"),a.dataset.wmapChartExpand="1",a.addEventListener("click",()=>openChartExpandModal(t,r.textContent??e)),i.appendChild(a),t.appendChild(i);const p=t.ownerDocument.createElement("div");Object.assign(p.style,{display:"flex",gap:m.sm,marginBottom:m.md,flexWrap:"wrap",alignItems:"center"}),t.appendChild(p);const c=t.ownerDocument.createElement("div");return Object.assign(c.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(c),{card:t,heading:r,controlsRow:p,body:c}}export function makeSegmented(e,o,n,t=document,i=!1){const r=t.createElement("div");r.setAttribute("role","radiogroup"),Object.assign(r.style,{display:"inline-flex",border:`1px solid ${f.menuBorder}`,borderRadius:C.control,overflow:"hidden"});const l=`seg-${Math.random().toString(36).slice(2,9)}`,s=[];return e.forEach(([a,p],c)=>{const d=t.createElement("label");Object.assign(d.style,{display:"inline-flex",alignItems:"center",fontSize:S.body,padding:i?"1px 6px":"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:c>0?`1px solid ${f.menuBorder}`:"none"}),L(d,"bare");const u=t.createElement("input");u.type="radio",u.name=l,u.value=a,u.checked=a===o,Object.assign(u.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const w=()=>{d.style.background=u.checked?f.bgActive:f.menuBg,d.style.color=u.checked?f.iconActive:f.text,d.dataset.on=u.checked?"true":"false"};s.push(w),w(),u.addEventListener("change",()=>{if(u.checked){for(const v of s)v();n(a)}}),d.append(u,t.createTextNode(p)),r.appendChild(d)}),r}export function makeBackButton(e,o=document){const n=o.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{...F("outlined"),background:f.menuBg,color:f.label}),L(n),n.addEventListener("click",e),n}export function renderEmptyState(e,o,n){const t=e.ownerDocument.createElement("div");t.textContent=o,Object.assign(t.style,{color:f.label,fontSize:S.body,padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeListSelect(e,o,n,t={}){const{maxWidth:i="200px",ownerDocument:r=document,ariaLabel:l,emptyText:s,searchPlaceholder:a="Filter\u2026",hook:p}=t;let c=o;const d=x=>e.find(O=>O.value===x)?.label??"",u=r.createElement("button");u.type="button",p&&(u.dataset.wmapSelect=p),l&&u.setAttribute("aria-label",l),Object.assign(u.style,{fontSize:S.body,padding:`${m.xxs} ${m.sm}`,background:f.menuBg,color:f.text,border:`1px solid ${f.menuBorder}`,borderRadius:C.control,maxWidth:i,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:m.sm,justifyContent:"space-between"}),L(u),R(u,!1);const w=r.createElement("span");Object.assign(w.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const v=r.createElement("span");v.textContent="\u25BE",v.style.flex="0 0 auto",u.append(w,v);const E=()=>{w.textContent=e.length===0?s??"(none)":d(c)||s||"(none)"};if(E(),e.length===0)return u.disabled=!0,u.style.opacity="0.6",u.style.cursor="default",Object.defineProperty(u,"value",{get:()=>c,set:x=>{c=x,E()}});let b=null;const B=()=>{b&&(b.remove(),b=null,R(u,!1))},$=()=>{B(),u.focus()};function U(){const x=u.getBoundingClientRect(),O=r.defaultView??window,D=Math.max(x.width,220),q=Math.min(x.left,Math.max(4,(O.innerWidth??1/0)-D-4));b=r.createElement("div"),b.setAttribute("role","listbox"),b.setAttribute("aria-label",l??"Options"),Object.assign(b.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${q}px`,background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:C.control,boxShadow:N.menu,zIndex:K,minWidth:`${D}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const T=[],I=()=>T.filter(h=>h.row.style.display!=="none").map(h=>h.row);if(e.length>W){const h=J(y=>{for(const g of T)g.row.style.display=g.label.includes(y)?"":"none"},a,r);Object.assign(h.style,{position:"sticky",top:"0",zIndex:"1",background:f.menuBg}),h.addEventListener("keydown",y=>{if(y.key==="Escape"){$();return}y.key==="ArrowDown"&&(y.preventDefault(),I()[0]?.focus())}),b.appendChild(h)}for(const h of e){const y=h.value===c,g=r.createElement("div");g.textContent=h.label,g.setAttribute("role","option"),g.setAttribute("aria-selected",y?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:`${m.sm} ${m.xl}`,fontSize:S.body,cursor:"pointer",color:y?f.iconActive:f.text,fontWeight:y?"700":"400",background:y?f.menuActive:"transparent",whiteSpace:"nowrap"}),g.addEventListener("mouseenter",()=>{y||(g.style.background=f.menuHover)}),g.addEventListener("mouseleave",()=>{y||(g.style.background="transparent")}),g.addEventListener("click",X=>{X.stopPropagation(),c=h.value,E(),$(),n(h.value)}),b.appendChild(g),T.push({row:g,label:h.label.toLowerCase()})}if(Z(b,I,$),Y(u).appendChild(b),R(u,!0),e.length<=W){const h=e.findIndex(y=>y.value===c);I()[h>=0?h:0]?.focus()}}u.addEventListener("click",x=>{if(x.stopPropagation(),b){B();return}U()});const M=x=>{if(!u.isConnected){r.removeEventListener("click",M);return}b&&!b.contains(x.target)&&!u.contains(x.target)&&B()};return r.addEventListener("click",M),Object.defineProperty(u,"value",{get:()=>c,set:x=>{c=x,E()}})}export function testOptionLabels(e){const o=e.some(n=>n.derived);return e.map(n=>(o&&!n.derived?le:"")+re(n,n.testNumber))}export function makeTestSelect(e,o,n,t={}){const{maxWidth:i="200px",emptyText:r="No parametric tests",ownerDocument:l=document}=t;return makeListSelect(testOptionLabels(e).map((s,a)=>({value:String(e[a].testNumber),label:s})),o!==null?String(o):"",s=>n(Number(s)),{maxWidth:i,ownerDocument:l,ariaLabel:"Test",emptyText:r,searchPlaceholder:"Filter tests\u2026"})}export function makeLinkedAxisPrefs(e,o,n,t=document){let i=o?.includeLimits,r=o?.clipOutliers??!1,l=o?.limits??"both";const s=()=>n({includeLimits:i,clipOutliers:r,limits:l});return{get:()=>({includeLimits:i,clipOutliers:r,limits:l}),sync(a,p,c=!1){e.innerHTML="",c&&e.appendChild(makeLimitsSelect(l,d=>{l=d,s()},t)),p&&e.appendChild(makeToggle("Axis includes limits",a,d=>{i=d,s()},t)),e.appendChild(makeToggle("Clip outliers",r,d=>{r=d,s()},t))},set(a){const p=a.limits??"both";return a.includeLimits===i&&a.clipOutliers===r&&p===l?!1:(i=a.includeLimits,r=a.clipOutliers,l=p,!0)}}}export function makeLinkedTestSelect(e,o,n,t={}){let i=o;const r=makeTestSelect(e,i,l=>{l!==i&&(i=l,n(l))},t);return{el:r,get:()=>i,set(l){return i===l||!e.some(s=>s.testNumber===l)?!1:(i=l,r.value=String(l),!0)}}}export const ALL_GROUPS="\0all";export function makeLinkedGroupSelect(e,o,n,t,i={}){let r=n;const l=c=>c??ALL_GROUPS,s=c=>c===ALL_GROUPS?null:c,a=makeLabeledSelect(o,[{value:ALL_GROUPS,label:"All groups"},...e.map(c=>({value:c,label:c}))],l(r),c=>{const d=s(c);d!==r&&(r=d,t(d))},{...i,hook:"group-scope"}),p=a.querySelector('[data-wmap-select="group-scope"]');return{el:a,get:()=>r,set(c){return r===c||c!==null&&!e.includes(c)?!1:(r=c,p&&(p.value=l(c)),!0)}}}export function makeWaferSelect(e,o,n,t={}){const{maxWidth:i="160px",allLabel:r="All wafers",ownerDocument:l=document}=t,s="\0all";return makeListSelect([{value:s,label:r},...e.map((a,p)=>({value:String(p),label:a.label??`#${p}`}))],o===null?s:String(o),a=>n(a===s?null:Number(a)),{maxWidth:i,ownerDocument:l,ariaLabel:r,searchPlaceholder:"Filter wafers\u2026"})}export function makeToggle(e,o,n,t=document){const i=t.createElement("label");Object.assign(i.style,{display:"inline-flex",alignItems:"center",gap:m.xs,fontSize:S.body,color:f.label,cursor:"pointer",userSelect:"none"}),L(i,"bare");const r=t.createElement("input");return r.type="checkbox",r.checked=o,r.style.cssText="margin:0;cursor:pointer;",r.addEventListener("change",()=>n(r.checked)),i.append(r,t.createTextNode(e)),i}export function makeLabeledSelect(e,o,n,t,i={}){const{maxWidth:r="160px",hook:l,ownerDocument:s=document}=i,a=s.createElement("label");a.textContent=e,Object.assign(a.style,{color:f.label,fontSize:S.body,display:"flex",alignItems:"center",gap:m.xs});const p=makeListSelect(o,n,t,{maxWidth:r,ownerDocument:s,hook:l,ariaLabel:e.replace(/:\s*$/,"")});return a.appendChild(p),a}export function makeChartGridWrap(e=document){const o=e.createElement("div");return o.dataset.wmapChartGrid="1",Object.assign(o.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:m.lg,flex:"0 0 auto"}),o}export function makeTooltip(e){const o=e.ownerDocument.createElement("div");return Object.assign(o.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:"rgba(30, 32, 40, 0.93)",color:"#f0f0f2",border:"1px solid rgba(255,255,255,0.10)",borderRadius:C.control,padding:`${m.sm} ${m.xl}`,fontSize:S.sub,fontFamily:S.family,maxWidth:"320px",whiteSpace:"normal",overflowWrap:"break-word",boxShadow:N.menu}),e.appendChild(o),o}export function attachChartTip(e,o,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",i=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,o,i.clientX,i.clientY)}),e.addEventListener("mousemove",i=>{n.style.display==="block"&&positionChartTooltip(n,o,i.clientX,i.clientY)}),e.addEventListener("mouseleave",()=>{n.style.display="none"}),e.addEventListener("click",()=>{n.style.display="none"}),e.addEventListener("focus",()=>{n.textContent=t,n.style.display="block";const i=e.getBoundingClientRect();positionChartTooltip(n,o,i.left+i.width/2,i.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,o,n,t){const i=o.getBoundingClientRect(),r=14,l=4;e.style.left="0",e.style.top="0";const s=e.offsetWidth,a=e.offsetHeight;let p=n-i.left+r,c=t-i.top+r;p+s+l>i.width&&(p=n-i.left-r-s),c+a+l>i.height&&(c=t-i.top-r-a),p=Math.max(l,p),c=Math.max(l,c),e.style.left=`${p}px`,e.style.top=`${c}px`}export const LIMIT_LABEL={test:{lo:"Lo limit",hi:"Hi limit"},spec:{lo:"LSL",hi:"USL"}},LIMIT_DASH={test:[3,3],spec:[10,4]},LIMITS_SHOWN_OPTIONS=[{value:"both",label:"Test + spec"},{value:"test",label:"Test limits"},{value:"spec",label:"Spec limits (LSL/USL)"},{value:"none",label:"None"}];function A(e,o){const[n,t]=o==="test"?[e?.limitLow,e?.limitHigh]:[e?.specLow,e?.specHigh],i=[];return n!==void 0&&Number.isFinite(n)&&i.push({value:n,kind:o,end:"lo",label:LIMIT_LABEL[o].lo}),t!==void 0&&Number.isFinite(t)&&i.push({value:t,kind:o,end:"hi",label:LIMIT_LABEL[o].hi}),i}export function hasBothLimitKinds(e){return A(e,"test").length>0&&A(e,"spec").length>0}export function limitLines(e,o="both"){if(o==="none")return[];const n=A(e,"test"),t=A(e,"spec"),i=o==="test"?n:o==="spec"?t:[...n,...t];return i.length?i:[...n,...t]}export function limitExtent(e){if(!e.length)return{};const o=e.map(n=>n.value);return{lo:te(o),hi:ne(o)}}export function stackLabelRows(e,o=4){const n=e.map((r,l)=>({...r,i:l})).sort((r,l)=>r.start-l.start),t=[],i=new Array(e.length).fill(0);for(const r of n){let l=t.findIndex(s=>r.start>=s+o);l===-1?(l=t.length,t.push(r.end)):t[l]=r.end,i[r.i]=l}return i}export function fillTextOnHalo(e,o,n,t,i){if(i){const r=e.measureText(o),l=2,s=e.fillStyle;e.fillStyle=i,e.fillRect(n-r.actualBoundingBoxLeft-l,t-r.actualBoundingBoxAscent-l,r.actualBoundingBoxLeft+r.actualBoundingBoxRight+2*l,r.actualBoundingBoxAscent+r.actualBoundingBoxDescent+2*l),e.fillStyle=s}e.fillText(o,n,t)}export function strokeLimitLine(e,o,n,t,i,r,l){e.save(),e.strokeStyle=n,e.lineWidth=1,e.setLineDash(LIMIT_DASH[o.kind]),e.beginPath(),e.moveTo(t,i),e.lineTo(r,l),e.stroke(),e.restore()}export function makeLimitsSelect(e,o,n=document){return makeLabeledSelect("Limits:",LIMITS_SHOWN_OPTIONS,e,t=>o(t),{maxWidth:"170px",hook:"limits-shown",ownerDocument:n})}export{P as robustFence};export function shouldIncludeLimitsByDefault(e,o,n,t,i=1/3){if(n===void 0&&t===void 0||!Number.isFinite(e)||!Number.isFinite(o))return!1;const r=Math.min(e,n??e),s=Math.max(o,t??o)-r;if(s<=0)return!0;const a=o-e;return a===0?!0:a/s>=i}export function resolveAxisRange(e){const{dataMin:o,dataMax:n,limits:t=[],includeLimits:i,clipOutliers:r,values:l}=e;let s=o,a=n,p=0;if(r&&l?.length){const d=P(l);if(d){const u=Math.max(s,d.lo),w=Math.min(a,d.hi);w>u&&(p=l.filter(v=>Number.isFinite(v)&&(v<u||v>w)).length,s=u,a=w)}}if(i)for(const d of t)s=Math.min(s,d.value),a=Math.max(a,d.value);s===a&&(s-=1,a+=1);const c=[];for(const d of t)d.value<s?c.push({value:d.value,label:d.label,side:"lo"}):d.value>a&&c.push({value:d.value,label:d.label,side:"hi"});return{lo:s,hi:a,offAxis:c,clippedCount:p}}export function limitLabelSide(e,o,n,t,i,r=3){const l=i?-1:1;return(l<0?e-r-o>=n:e+r+o<=t)?l:-l}export function drawOffAxisLimits(e,o,n,t,i,r,l){if(!o.length)return;e.save(),e.fillStyle=i,e.font=`${k(-1)}px system-ui, sans-serif`;const s={lo:0,hi:0},a=k(-1)+2;for(const{value:p,label:c,side:d}of o){const u=s[d]++*a,w=t==="horizontal"?d==="lo"?"\u2190":"\u2192":d==="lo"?"\u2193":"\u2191",v=d==="lo"?`${w} ${c} ${r(p)}`:`${c} ${r(p)} ${w}`;t==="horizontal"?(e.textBaseline="top",e.textAlign=d==="lo"?"left":"right",fillTextOnHalo(e,v,d==="lo"?n.left+2:n.right-2,n.top+2+u,l)):(e.textBaseline=d==="lo"?"bottom":"top",e.textAlign="left",fillTextOnHalo(e,v,n.left+4,d==="lo"?n.bottom-2-u:n.top+2+u,l))}e.restore()}