@wafertools/wafermap 0.30.1 → 0.30.2

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 (38) hide show
  1. package/AGENTS.md +59 -10
  2. package/CHANGELOG.md +80 -8
  3. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  4. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  6. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  8. package/dist/packages/canvas-adapter/chunked.d.ts +45 -0
  9. package/dist/packages/canvas-adapter/chunked.js +1 -0
  10. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  11. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +55 -0
  12. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  13. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  14. package/dist/packages/canvas-adapter/summaryPanel.d.ts +55 -9
  15. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  16. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  17. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  18. package/dist/packages/canvas-adapter/userGuideHtml.js +1 -1
  19. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  20. package/dist/packages/canvas-adapter/version.js +1 -1
  21. package/dist/packages/core/aggregates.js +1 -1
  22. package/dist/packages/core/utils.d.ts +55 -0
  23. package/dist/packages/core/utils.js +1 -1
  24. package/dist/packages/renderer/binColors.d.ts +14 -0
  25. package/dist/packages/renderer/binColors.js +1 -1
  26. package/dist/packages/renderer/buildView.js +1 -1
  27. package/dist/packages/renderer/buildWaferMap.js +1 -1
  28. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  29. package/dist/packages/stats/capability.d.ts +56 -0
  30. package/dist/packages/stats/capability.js +1 -1
  31. package/dist/packages/stats/correlation.d.ts +30 -0
  32. package/dist/packages/stats/correlation.js +1 -1
  33. package/dist/packages/stats/findingsNarrative.js +1 -1
  34. package/dist/packages/stats/math.d.ts +27 -2
  35. package/dist/packages/stats/math.js +1 -1
  36. package/dist/packages/stats/renderSummaryReport.js +4 -4
  37. package/llms.txt +12 -7
  38. package/package.json +2 -2
package/AGENTS.md CHANGED
@@ -25,8 +25,24 @@ loudly over guessing.
25
25
  - `@wafertools/wafermap/worker` — `createWafermapWorker()` for off-main-thread builds.
26
26
 
27
27
  Default path: `buildWaferMap()` once when data loads, then `renderWaferMap()` for a
28
- single wafer or `renderWaferGallery()` for several. Do not use `toCanvas()`, `buildView()`
29
- or the other low-level pipeline functions: they are deprecated and removed in 0.31.0.
28
+ single wafer or `renderWaferGallery()` for several.
29
+
30
+ **The types still export a large deprecated surface that is removed in 0.31.0 — do not
31
+ reach into it just because autocomplete offers it.** Four groups, all replaced by the
32
+ default path above:
33
+
34
+ - the low-level drawing pipeline — `buildView()`, `toCanvas()`, `createWafer()`,
35
+ `generateDies()`, and the geometry and transform helpers around them;
36
+ - the chart-data builders — `buildYieldData()`, `buildCorrelationMatrix()`,
37
+ `buildTestBoxplotData()` and the rest: these were the internals of the Insights tab,
38
+ which the renderers now mount for you (see below);
39
+ - the region builders — `buildRingRegions()`, `buildQuadrantRegions()` and friends:
40
+ region yield comes back from `analyzeWaferMap()`;
41
+ - per-die and per-colour helpers — `getDieTestValue()`, `buildHoverText()`,
42
+ `resolveBinColors()`, `getValueColorScheme()`, `valueToViridis()`.
43
+
44
+ If the only way to do something is through one of these, that is a library gap worth
45
+ reporting, not a pattern to build on.
30
46
 
31
47
  ### Traps that produce silently wrong maps
32
48
 
@@ -37,8 +53,20 @@ or the other low-level pipeline functions: they are deprecated and removed in 0.
37
53
  through unchanged; `dieConfig.width`/`height` convert to physical units. Do not
38
54
  pre-multiply. The geometry inputs are `waferConfig` (type `WaferConfig`) and
39
55
  `dieConfig` (type `DieConfig`) — both optional, both inferred when omitted.
40
- - **`passBins` decides both the yield number and the wording of its label.** Set it
41
- from the actual test program. Do not assume `[1]`.
56
+ - **Bins and test values must be numbers, and verdicts booleans.** Every parser —
57
+ CSV, JSON, a spreadsheet export — hands you `"1"`, and `"1"` is not pass bin 1: those
58
+ dies count as fails and the yield is wrong, while a test value left as text is not
59
+ plotted or analysed correctly. Convert with `Number()` at parse time. `buildWaferMap`
60
+ samples the input and reports `input-values-not-numbers` (severity `'error'`) rather
61
+ than coercing behind your back, so a build that "works" can still be wrong — read the
62
+ warnings.
63
+ - **`passBins` and `ringCount` are set once, on `buildWaferMap`, and travel on the
64
+ result.** Neither is an option on `analyzeWaferMap`, `analyzeWaferLot`,
65
+ `renderWaferMap` or `renderWaferGallery` — passing one there is a type error in
66
+ TypeScript, and in JavaScript it is ignored with an `analysis-option-corrected`
67
+ warning while the real value is read from the map. `passBins` decides both the yield
68
+ number and the wording of its label, so set it from the actual test program: do not
69
+ assume `[1]`, and never re-default to `[1]` downstream — read `result.passBins`.
42
70
  - **`testValues` is keyed by test number**, e.g. `{ 1050: 0.42 }` — not a positional
43
71
  array. `activeTest` likewise takes a *test number* (`1050`), not an index.
44
72
  - **Functional tests (`testType: 'F'`) have no measured value.** Read their verdicts
@@ -88,7 +116,26 @@ or the other low-level pipeline functions: they are deprecated and removed in 0.
88
116
  your own (a table swatch, an export)? Read `controller.getBinColors()` for a live map,
89
117
  or `binColorsForMaps(results)` — never a palette lookup of your own.
90
118
  - Build once, render many: `buildWaferMap()` handles data + geometry; re-render UI
91
- changes through the controller's `setOptions()`, not by rebuilding.
119
+ changes through the controller's `setOptions()`, not by rebuilding. New data for a
120
+ map that is already mounted goes through `setResult()` — do not `destroy()` and
121
+ remount.
122
+ - **The analysis surfaces are already built — do not reimplement them.** Pass
123
+ `statsSummary` to `renderWaferMap` and it mounts the Summary panel; pass
124
+ `insights: { enabled: true }` and it mounts the chart suite (yield, bin pareto,
125
+ boxplot, histogram, correlation, scatter, capability). `renderWaferGallery` takes the
126
+ same option across a whole lot. Supply or replace the analysis later with
127
+ `setStatsSummary()`. Hand-building those charts is what the deprecated chart-data
128
+ builders were for, and they go in 0.31.0.
129
+ - **Click-to-highlight is wired, not hand-rolled.** `onSelect` reports what the user
130
+ picked; `setSelection(dies)` / `clearSelection()` drive it from your own UI — for
131
+ example from a finding, whose `dieKeys` match `getDieKey(die)` exactly.
132
+ - A die layout with no test data — a map of the reticle or the grid alone — is
133
+ `buildWaferMap({ layout: true, waferConfig, dieConfig })`, not a synthesized results
134
+ array.
135
+ - `valueColorScheme` and `reverseValueScheme` travel as a pair. Every built-in gradient
136
+ but `'traffic'` and `'jet'` reads low = dark, high = light; if you draw your own
137
+ colorbar or swatch, resolve the colour through `resolveValueColorFn(name, reversed)`
138
+ so it cannot disagree with the dies.
92
139
  - `result.view` is internal. Use the promoted fields: `result.plotMode`,
93
140
  `result.metadata`, `result.isLotStack`, `result.hbinDefs`, `result.sbinDefs`,
94
141
  `result.testDefs`.
@@ -135,10 +182,10 @@ it is handed, because it has no way to know which tests anyone will look at.
135
182
  - **A Web Worker buys responsiveness, not speed.** `createWafermapWorker` copies data
136
183
  across `postMessage`, so total time goes *up*. Use it when a build would otherwise
137
184
  visibly freeze the page, not for small datasets.
138
- - **Capability, pass rates and region yield come back from the analysis** —
139
- `stats.capability` (with `computePerTestStats`), `stats.testSpecYield`,
140
- `stats.testFlagYield`, `stats.functionalYield` and `stats.regionYield`, on wafer and
141
- lot summaries alike. Do not compute Cp/Cpk or ring yield yourself: the pooled
185
+ - **Capability, pass rates, region yield and the spatial-pattern label come back from
186
+ the analysis** — `stats.capability` (with `computePerTestStats`), `stats.testSpecYield`,
187
+ `stats.testFlagYield`, `stats.functionalYield`, `stats.regionYield` and
188
+ `stats.spatialPattern`, on wafer and lot summaries alike. Do not compute Cp/Cpk or ring yield yourself: the pooled
142
189
  within-wafer stddev and per-wafer pass bins are easy to get subtly wrong.
143
190
  - **Reports from code: `renderWaferReportHtml(result, summary)` and
144
191
  `renderLotReportHtml(results)`** — they take the built maps, so pass bins and ring
@@ -157,7 +204,7 @@ it is handed, because it has no way to know which tests anyone will look at.
157
204
  | `WaferMapResult.inference.warnings` | `WaferMapResult.warnings` (structured, with a `code`) |
158
205
  | `ViewOptions.testIndex` | `activeTest` |
159
206
  | `mountWaferCanvas` | `renderWaferMap` |
160
- | `HARD_BIN_COLORS` / `SOFT_BIN_COLORS` | `BIN_PALETTE` |
207
+ | `HARD_BIN_COLORS` / `SOFT_BIN_COLORS` | `BinDef.color`, or `registerBinColorScheme` — there is no exported palette constant |
161
208
  | `GalleryItem` | `WaferMapDisplayItem` |
162
209
  | `MountOptions` | `RenderOptions` |
163
210
  | `WaferCanvasController` | `WaferMapController` |
@@ -172,6 +219,8 @@ it is handed, because it has no way to know which tests anyone will look at.
172
219
  | `plotMode: 'specLimit'` | `passFailDisplay: 'spec'` |
173
220
  | standalone `getDieAtPoint` | `onHover` / `onClick` on `renderWaferMap` |
174
221
  | `RenderOptions.tooltipTestLimit` | (was a no-op; nothing replaces it) |
222
+ | `enableYieldAnalysis` / `enableHardBinAnalysis` / `enableSoftBinAnalysis` / `enableReticlePositionAnalysis` / `enableTestSiteAnalysis` / `enableClusterAnalysis` / `enableAngularAnalysis` / `enablePatternClassification` | nothing — every analysis runs; scope cost with `testNumbers` instead |
223
+ | `WaferMapController.setIdentityVisible` | `showIdentity` in `RenderOptions` |
175
224
 
176
225
  Passing a removed option is a type error, and is ignored at runtime. Do not add
177
226
  compatibility shims for them.
package/CHANGELOG.md CHANGED
@@ -22,18 +22,90 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
25
+ ## [0.30.2] — 2026-09-20
26
+
27
+ ### Added
28
+
29
+ - **A gallery now reports its own progress, so a host's indicator can be honest about a long
30
+ load.** `GalleryOptions` gains two callbacks (API §6.2):
31
+ - `onItemResolved(resolved, total)` — the **advance** signal, fired as each card is built.
32
+ `total` is the count you passed, so it is right from the first call and a progress bar can be
33
+ sized before anything arrives.
34
+ - `onItemsResolved()` — the **settled** signal, fired once the gallery has finished: every
35
+ factory resolved *and* the lot-wide legend, shared colours and Summary panel up to date. On a
36
+ large lot the panel keeps filling in for seconds after the last card, so this is deliberately
37
+ later than "the cards are in" — a host clearing its indicator when the cards land leaves the
38
+ rest of the wait unexplained.
39
+ - Both fire whichever form the items took, so no host branches on the path the gallery chose: a
40
+ fully pre-built mount is one `onItemResolved` call with `resolved === total`, and
41
+ `onItemsResolved` always fires asynchronously, after `renderWaferGallery` has returned. Both
42
+ fire again on a rebuild (`setItems`, or switching into a stacked mode).
43
+ - **`CorrelationMatrix.sample`** — present only when the matrix was computed from a sample of the
44
+ dies, with `of` (the dies that carried test values) and `used` (how many were read).
45
+ The Insights correlation panel labels a sampled matrix from it; a host reading the matrix
46
+ directly must do the same.
47
+
48
+ ### Changed
49
+
50
+ - **Correlation is computed from at most 25,000 dies.** It is the only analysis here that is
51
+ quadratic in tests and linear in dies — a 400,000-die, 50-test lot is 490 million pair updates,
52
+ which in a browser presents as a panel that never renders. Above that budget the dies are
53
+ sampled by an even stride across the whole population, never a prefix (dies arrive grouped by
54
+ wafer, so the first 25,000 of a lot are its first few wafers and would describe those rather
55
+ than the lot). At 25,000 dies the standard error of `r` is under 0.007, so the coefficients are
56
+ unchanged at the precision they are displayed to. **A sampled matrix always says so** — in the
57
+ Insights panel and in `CorrelationMatrix.sample`.
58
+ - **The lot and wafer Summary panels are built across tasks instead of in one block, and fill in
59
+ section by section.** Pooling every die of every wafer and deriving the bin, region and
60
+ per-test sections from it is the longest single piece of work this library does: 15.3 s on a
61
+ 50-wafer lot of 4,000 dies × 100 tests, which is where a browser starts offering to kill the
62
+ page. It is now around 335 steps of a few hundred ms at most. An ordinary lot still renders in
63
+ one synchronous pass and looks exactly as it did. A panel render in flight is abandoned when
64
+ the lot, filter or highlight changes, and on `destroy()`.
65
+ - **The same panel work is also about 2.3× cheaper, and a lot pays for it once.** The per-test
66
+ pass over a lot's pooled dies is memoised on the population it describes, so the lot Summary
67
+ panel, the Insights Overview and the Insights capability chart share one computation instead of
68
+ running three; the sort inside it uses a `Float64Array` (4× on the whole pooled pass); and the
69
+ Test Values rows are built in one pass instead of three.
70
+ - **Switching Insights tabs no longer rebuilds the panel you are returning to.** A built section
71
+ is kept per view, so going back to Distributions is instant rather than re-running capability,
72
+ boxplot, histogram and trend (about 5 s at 400,000 dies). Only a pure tab switch reuses it —
73
+ new data, Group by, scope or an axis preference still rebuilds everything.
74
+ - **A lot-wide option is pushed to the cards only when its value actually changed.** `binColors`,
75
+ `valueRange` and `metadataValueOrder` are re-derived from the whole population as each wafer
76
+ resolves, and each derivation allocates a fresh object, so every card was being rebuilt and
77
+ redrawn for an identical value. On a progressive load that is quadratic: on 50 wafers × 8,000
78
+ dies × 50 tests in Chrome the bin-colour map alone was 1,275 pushes and 24 s of a 64 s load,
79
+ and is now one push.
80
+ - **The gallery's lot Summary panel now settles once, when the last wafer resolves, instead of
81
+ re-rendering after every one.** A progressive load was re-pooling and redrawing the whole
82
+ panel on each newly resolved wafer, which is quadratic in wafer count and was the single
83
+ largest cost on this path: 175 s of a 182 s, 50-card progressive load. The panel still renders
84
+ once at mount and again whenever a user opens it. Combined with the change above, 50 cards of
85
+ 8,000 dies now load in **7.7 s of non-blocking work with no task over 440 ms**, down from
86
+ 182 s.
87
+
88
+ ### Fixed
89
+
90
+ - **A large wafer or lot could fail outright with "Maximum call stack size exceeded".**
91
+ `Math.min(...)`/`Math.max(...)` spread a whole die array into a call, which throws once the
92
+ array passes roughly 100,000 entries — reachable on a single 400,000-die wafer in aggregated
93
+ values, inferred wafer geometry, reticle bounds and several chart scales. Every such call now
94
+ uses the `minOf`/`maxOf` helpers, and `scripts/check-spread-limits.mjs` (wired into
95
+ `npm run check`) fails the build if one comes back.
96
+ - **A non-finite test value could make a whole test's statistics `NaN`.** `computePerTestStats`
97
+ sorted and summarised whatever `testValues` held, with no `Number.isFinite` screen, so one
98
+ `NaN` or `Infinity` from a parser silently turned that test's mean, median, sigma and Cpk into
99
+ `NaN` rather than being dropped as no data.
100
+
25
101
  ## [0.30.1] — 2026-09-16
26
102
 
27
103
  ### Security
28
104
 
29
- - **Names from a data file could run script in a tooltip.** The die tooltip and the Insights
30
- chart tooltips (correlation, capability, boxplot, trend, histogram, bar and grouped-bar
31
- charts), plus the legend-row and toolbar tooltips, built HTML from test names, units, bin
32
- names, wafer labels and metadata values without escaping them. A file with a test named
33
- `<img src=x onerror=…>` ran that script when a user hovered, in a Tauri, Electron or
34
- WebView2 host inside the app's own webview. Every such value is now escaped. The escape
35
- helper, formerly private to the reports, is the one copy for the library (`core/utils.ts`),
36
- and `tests/htmlEscaping.test.mjs` fails if a tooltip puts a raw label into HTML again.
105
+ - **Fixes a security issue in how names from a data file are displayed.** Earlier versions are
106
+ affected; update to 0.30.1, particularly if your app opens files from sources you don't
107
+ control. Names now always display as plain text, so a name that contains markup shows it
108
+ literally.
37
109
 
38
110
  ### Changed
39
111
 
@@ -1 +1 @@
1
- import{buildTestBoxplotData as Ze}from"../../stats/boxplot.js";import{SPACE as De,fontPx as N,FONT as et,CLR as tt}from"../toolbar.js";import{fmt as xe}from"../../renderer/fmt.js";import{QUANTITY as Ie}from"./palette.js";import{cardShell as nt,observeResize as ot,makeTooltip as it,positionChartTooltip as lt,makeBackButton as st,makeLinkedTestSelect as at,makeToggle as rt,makeLinkedAxisPrefs as ct,renderEmptyState as He,growCardToFitContent as ft,resolveChartCanvasColors as mt,makeAxisFormat as ut,resolveAxisRange as dt,shouldIncludeLimitsByDefault as xt,drawOffAxisLimits as pt,limitLabelSide as ht,PADDING as L,VALUE_WIDTH as gt,prepareCanvas as bt}from"./chartShell.js";import{escHtml as M}from"../../core/utils.js";const k=24,C=5,Re=110,Tt=12,Oe=20;export function renderBoxplotPanel(h){const{title:Z="Test value distribution",items:We,testDefs:pe,onSaveImage:Ee,groups:u,groupLabelText:ee="group",onOpen:q}=h,Pe=h.openActionLabel??"open that wafer",_e=h.openActionLabel??"open this wafer",{card:a,heading:Ge,body:d,controlsRow:Y}=nt(Z,Ee,h.ownerDocument);a.style.flex="0 0 auto",d.style.flex="0 0 auto",a.style.alignSelf="start";const te=pe.filter(n=>n.testNumber!==void 0);let v=h.selectedTestNumber??te[0]?.testNumber??null,ne=!1,Q=0,x=null,H=null,oe=null,he=null;function Xe(){const n=!u||u.length===0?"":x??"\0overview";if(oe&&he===n)return oe;const m=!u||u.length===0?We:x!==null?u.find(g=>g.key===x)?.items??[]:u.map(g=>({label:g.key,dies:g.items.flatMap(c=>c.dies??[])}));return oe=m,he=n,m}function ge(){x!==null&&!H?(H=st(()=>{be(null),h.onGroupChange?.(null)},a.ownerDocument),Y.appendChild(H)):x===null&&H&&(H.remove(),H=null),Ge.textContent=x!==null?`${Z} \u2014 ${ee}: ${x}`:Z}function be(n){return x===n||n!==null&&!(u??[]).some(m=>m.key===n)?!1:(x=n,ge(),U(),S(),!0)}const Te=at(te,v,n=>{v=n,S(),h.onTestChange?.(n)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Ne=Te.el;Y.appendChild(Ne),Y.appendChild(rt("Log scale",ne,n=>{ne=n,S()},a.ownerDocument));const ie=a.ownerDocument.createElement("span");Object.assign(ie.style,{display:"inline-flex",gap:De.lg,alignItems:"center"}),Y.appendChild(ie);const le=ct(ie,h.axisPrefs,n=>{h.onAxisPrefsChange?.(n),S()},a.ownerDocument),se=a.ownerDocument.createElement("div");Object.assign(se.style,{color:tt.label,fontSize:et.body,marginBottom:De.sm}),a.insertBefore(se,d);function U(){const n=!!u&&u.length>0&&x===null,m=[];n?m.push(`click a ${ee}'s box to see it by wafer`):q&&m.push(`click a box to ${Pe}`);const g=m.length?`${m[0][0].toUpperCase()}${m[0].slice(1)} \xB7 `:"";se.textContent=`${g}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(Q?` \xB7 axis clipped, ${Q} value${Q===1?"":"s"} outside`:"")}U(),ge();const O=it(a);let ae=null;function S(){const{includeLimits:n,clipOutliers:m}=le.get();if(U(),d.innerHTML="",te.length===0||v===null){He(d,"No parametric test data available for box plots.");return}const g=Xe(),c=Ze(g,v),re=pe.find(t=>t.testNumber===v),z=re?.unit,F=re?.limitLow,V=re?.limitHigh;if(c.every(t=>t.count===0)){He(d,"No parametric test data available for box plots.");return}const p=a.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",d.appendChild(p);const ye=L*2+Math.min(c.length,Tt)*(k+C)+Oe;d.style.maxHeight=`${ye}px`,d.style.overflowY="auto",d.style.scrollbarGutter="stable";let W=-1;const ce=c.filter(t=>t.count>0),ve=Math.min(...ce.map(t=>t.min)),we=Math.max(...ce.map(t=>t.max)),Le=n??xt(ve,we,F,V);le.sync(Le,F!==void 0||V!==void 0);const E=dt({dataMin:ve,dataMax:we,limitLow:F,limitHigh:V,includeLimits:Le,clipOutliers:m,values:ce.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});Q=E.clippedCount,U();const R=E.lo,fe=E.hi,Me=fe-R||1,j=ne&&R>0,me=j?Math.log10(R):0,ke=(j?Math.log10(fe):0)-me||1,ue=ut(Math.max(Math.abs(R),Math.abs(fe)),z);function Qe(){const t=L+Re,i=p.clientWidth-t-gt-L;return{plotX:t,plotMaxWidth:Math.max(10,i)}}function A(t,i,s){return j?i+(Math.log10(t)-me)/ke*s:i+(t-R)/Me*s}function Ue(){const t=[];for(let i=0;i<=4;i++)t.push(j?Math.pow(10,me+ke*i/4):R+Me*i/4);return t}function K(){const t=mt(a),i=d.clientWidth,s=L*2+c.length*(k+C)+Oe,$=bt(p,a,i,s);if(!$)return;const{ctx:e}=$;e.font=`${N(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:f}=Qe();c.forEach((o,b)=>{const y=L+b*(k+C),r=y+k/2;b===W&&(e.fillStyle=t.bgHover,e.fillRect(0,y-C/2,i,k+C)),e.fillStyle=t.text,e.textAlign="right";const Fe=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(Fe,L+Re-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const Ve=l+f,_=Je=>Math.min(Ve,Math.max(l,Je)),Ae=A(o.min,l,f),$e=A(o.max,l,f),B=_(Ae),G=_(A(o.q1,l,f)),Be=_(A(o.median,l,f)),de=_(A(o.q3,l,f)),D=_($e),je=Ae<B,Ke=$e>D,I=y+3,X=y+k-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(B,r),e.lineTo(G,r),e.moveTo(de,r),e.lineTo(D,r);const w=3;je?(e.moveTo(B+w,r-w),e.lineTo(B,r),e.lineTo(B+w,r+w)):(e.moveTo(B,I),e.lineTo(B,X)),Ke?(e.moveTo(D-w,r-w),e.lineTo(D,r),e.lineTo(D-w,r+w)):(e.moveTo(D,I),e.lineTo(D,X)),e.stroke(),e.fillStyle=Ie,e.globalAlpha=.35,e.fillRect(G,I,Math.max(1,de-G),X-I),e.globalAlpha=1,e.strokeStyle=Ie,e.strokeRect(G,I,Math.max(1,de-G),X-I),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Be,I),e.lineTo(Be,X),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(xe(o.median,z,"engineering"),l+f+10,r)});const T=L+c.length*(k+C);e.font=`${N(-1)}px system-ui, sans-serif`,pt(e,E.offAxis,{left:l,right:l+f,top:0,bottom:T},"horizontal",t.limitLine,o=>xe(o,z,"engineering"));const ze=new Set(E.offAxis.map(o=>o.value));for(const[o,b]of[[F,"LSL"],[V,"USL"]]){if(o===void 0||ze.has(o))continue;const y=A(o,l,f);e.strokeStyle=t.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(y,0),e.lineTo(y,T),e.stroke(),e.setLineDash([]),e.fillStyle=t.limitLine;const r=ht(y,e.measureText(b).width,l,l+f,b==="LSL");e.textAlign=r<0?"right":"left",e.textBaseline="bottom",e.fillText(b,y+r*3,T-1)}e.font=`${N(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,T),e.lineTo(l+f,T),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";for(const o of Ue()){const b=A(o,l,f);e.beginPath(),e.moveTo(b,T),e.lineTo(b,T+4),e.stroke(),e.fillText(ue.tick(o),b,T+6)}ue.unitLabel&&(e.textAlign="left",e.font=`${N(-1)}px system-ui, sans-serif`,e.fillText(`(${ue.unitLabel})`,l+f+22,T+6),e.font=`${N(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function Ce(t){const i=Math.floor((t-L+C/2)/(k+C));return i>=0&&i<c.length?i:-1}function P(t){return xe(t,z,"engineering")}const J=!!u&&u.length>0&&x===null,Se=t=>!J&&!!q&&g[t]?.waferIndex!==void 0;p.addEventListener("mousemove",t=>{const i=p.getBoundingClientRect(),s=Ce(t.clientY-i.top),$=s>=0&&c[s].count>0&&(J||Se(s));if(s!==W&&(W=s,p.style.cursor=$?"pointer":"default",K()),s>=0&&c[s].count>0){const e=c[s],l=J?`<br><em>click to see this ${M(ee)} by wafer</em>`:Se(s)?`<br><em>click to ${M(_e)}</em>`:"";O.innerHTML=`<strong>${M(e.label)}</strong> (${e.count} dies)<br>max ${M(P(e.max))}<br>q3 ${M(P(e.q3))}<br>median ${M(P(e.median))}<br>q1 ${M(P(e.q1))}<br>min ${M(P(e.min))}${l}`,O.style.display="block",lt(O,a,t.clientX,t.clientY)}else O.style.display="none"}),p.addEventListener("mouseleave",()=>{W!==-1&&(W=-1,K()),O.style.display="none"}),p.addEventListener("click",t=>{const i=p.getBoundingClientRect(),s=Ce(t.clientY-i.top);if(s===-1||c[s].count===0)return;if(J){const e=c[s].label;be(e),h.onGroupChange?.(e);return}const $=g[s]?.waferIndex;q&&$!==void 0&&v!==null&&q($,v)}),ae?.disconnect(),ae=ot(a,()=>K()),K(),ft(a,d,ye)}S();function qe(n){Te.set(n)&&(v=n,S())}function Ye(n){le.set(n)&&S()}return{card:a,setTest:qe,setAxisPrefs:Ye,destroy:()=>ae?.disconnect()}}
1
+ import{buildTestBoxplotData as Ze}from"../../stats/boxplot.js";import{SPACE as De,fontPx as N,FONT as et,CLR as tt}from"../toolbar.js";import{fmt as xe}from"../../renderer/fmt.js";import{QUANTITY as Ie}from"./palette.js";import{cardShell as nt,observeResize as ot,makeTooltip as it,positionChartTooltip as lt,makeBackButton as st,makeLinkedTestSelect as at,makeToggle as rt,makeLinkedAxisPrefs as ct,renderEmptyState as He,growCardToFitContent as ft,resolveChartCanvasColors as mt,makeAxisFormat as ut,resolveAxisRange as dt,shouldIncludeLimitsByDefault as xt,drawOffAxisLimits as pt,limitLabelSide as ht,PADDING as L,VALUE_WIDTH as gt,prepareCanvas as bt}from"./chartShell.js";import{escHtml as k,maxOf as Tt,minOf as yt}from"../../core/utils.js";const M=24,C=5,Oe=110,vt=12,Re=20;export function renderBoxplotPanel(h){const{title:Z="Test value distribution",items:We,testDefs:pe,onSaveImage:Ee,groups:u,groupLabelText:ee="group",onOpen:q}=h,Pe=h.openActionLabel??"open that wafer",_e=h.openActionLabel??"open this wafer",{card:a,heading:Ge,body:d,controlsRow:Y}=nt(Z,Ee,h.ownerDocument);a.style.flex="0 0 auto",d.style.flex="0 0 auto",a.style.alignSelf="start";const te=pe.filter(n=>n.testNumber!==void 0);let v=h.selectedTestNumber??te[0]?.testNumber??null,ne=!1,Q=0,x=null,H=null,oe=null,he=null;function Xe(){const n=!u||u.length===0?"":x??"\0overview";if(oe&&he===n)return oe;const m=!u||u.length===0?We:x!==null?u.find(g=>g.key===x)?.items??[]:u.map(g=>({label:g.key,dies:g.items.flatMap(c=>c.dies??[])}));return oe=m,he=n,m}function ge(){x!==null&&!H?(H=st(()=>{be(null),h.onGroupChange?.(null)},a.ownerDocument),Y.appendChild(H)):x===null&&H&&(H.remove(),H=null),Ge.textContent=x!==null?`${Z} \u2014 ${ee}: ${x}`:Z}function be(n){return x===n||n!==null&&!(u??[]).some(m=>m.key===n)?!1:(x=n,ge(),U(),S(),!0)}const Te=at(te,v,n=>{v=n,S(),h.onTestChange?.(n)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Ne=Te.el;Y.appendChild(Ne),Y.appendChild(rt("Log scale",ne,n=>{ne=n,S()},a.ownerDocument));const ie=a.ownerDocument.createElement("span");Object.assign(ie.style,{display:"inline-flex",gap:De.lg,alignItems:"center"}),Y.appendChild(ie);const le=ct(ie,h.axisPrefs,n=>{h.onAxisPrefsChange?.(n),S()},a.ownerDocument),se=a.ownerDocument.createElement("div");Object.assign(se.style,{color:tt.label,fontSize:et.body,marginBottom:De.sm}),a.insertBefore(se,d);function U(){const n=!!u&&u.length>0&&x===null,m=[];n?m.push(`click a ${ee}'s box to see it by wafer`):q&&m.push(`click a box to ${Pe}`);const g=m.length?`${m[0][0].toUpperCase()}${m[0].slice(1)} \xB7 `:"";se.textContent=`${g}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(Q?` \xB7 axis clipped, ${Q} value${Q===1?"":"s"} outside`:"")}U(),ge();const R=it(a);let ae=null;function S(){const{includeLimits:n,clipOutliers:m}=le.get();if(U(),d.innerHTML="",te.length===0||v===null){He(d,"No parametric test data available for box plots.");return}const g=Xe(),c=Ze(g,v),re=pe.find(t=>t.testNumber===v),z=re?.unit,F=re?.limitLow,V=re?.limitHigh;if(c.every(t=>t.count===0)){He(d,"No parametric test data available for box plots.");return}const p=a.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",d.appendChild(p);const ye=L*2+Math.min(c.length,vt)*(M+C)+Re;d.style.maxHeight=`${ye}px`,d.style.overflowY="auto",d.style.scrollbarGutter="stable";let W=-1;const ce=c.filter(t=>t.count>0),ve=yt(ce.map(t=>t.min)),we=Tt(ce.map(t=>t.max)),Le=n??xt(ve,we,F,V);le.sync(Le,F!==void 0||V!==void 0);const E=dt({dataMin:ve,dataMax:we,limitLow:F,limitHigh:V,includeLimits:Le,clipOutliers:m,values:ce.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});Q=E.clippedCount,U();const O=E.lo,fe=E.hi,ke=fe-O||1,j=ne&&O>0,me=j?Math.log10(O):0,Me=(j?Math.log10(fe):0)-me||1,ue=ut(Math.max(Math.abs(O),Math.abs(fe)),z);function Qe(){const t=L+Oe,i=p.clientWidth-t-gt-L;return{plotX:t,plotMaxWidth:Math.max(10,i)}}function A(t,i,s){return j?i+(Math.log10(t)-me)/Me*s:i+(t-O)/ke*s}function Ue(){const t=[];for(let i=0;i<=4;i++)t.push(j?Math.pow(10,me+Me*i/4):O+ke*i/4);return t}function K(){const t=mt(a),i=d.clientWidth,s=L*2+c.length*(M+C)+Re,$=bt(p,a,i,s);if(!$)return;const{ctx:e}=$;e.font=`${N(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:f}=Qe();c.forEach((o,b)=>{const y=L+b*(M+C),r=y+M/2;b===W&&(e.fillStyle=t.bgHover,e.fillRect(0,y-C/2,i,M+C)),e.fillStyle=t.text,e.textAlign="right";const Fe=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(Fe,L+Oe-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const Ve=l+f,_=Je=>Math.min(Ve,Math.max(l,Je)),Ae=A(o.min,l,f),$e=A(o.max,l,f),B=_(Ae),G=_(A(o.q1,l,f)),Be=_(A(o.median,l,f)),de=_(A(o.q3,l,f)),D=_($e),je=Ae<B,Ke=$e>D,I=y+3,X=y+M-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(B,r),e.lineTo(G,r),e.moveTo(de,r),e.lineTo(D,r);const w=3;je?(e.moveTo(B+w,r-w),e.lineTo(B,r),e.lineTo(B+w,r+w)):(e.moveTo(B,I),e.lineTo(B,X)),Ke?(e.moveTo(D-w,r-w),e.lineTo(D,r),e.lineTo(D-w,r+w)):(e.moveTo(D,I),e.lineTo(D,X)),e.stroke(),e.fillStyle=Ie,e.globalAlpha=.35,e.fillRect(G,I,Math.max(1,de-G),X-I),e.globalAlpha=1,e.strokeStyle=Ie,e.strokeRect(G,I,Math.max(1,de-G),X-I),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Be,I),e.lineTo(Be,X),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(xe(o.median,z,"engineering"),l+f+10,r)});const T=L+c.length*(M+C);e.font=`${N(-1)}px system-ui, sans-serif`,pt(e,E.offAxis,{left:l,right:l+f,top:0,bottom:T},"horizontal",t.limitLine,o=>xe(o,z,"engineering"));const ze=new Set(E.offAxis.map(o=>o.value));for(const[o,b]of[[F,"LSL"],[V,"USL"]]){if(o===void 0||ze.has(o))continue;const y=A(o,l,f);e.strokeStyle=t.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(y,0),e.lineTo(y,T),e.stroke(),e.setLineDash([]),e.fillStyle=t.limitLine;const r=ht(y,e.measureText(b).width,l,l+f,b==="LSL");e.textAlign=r<0?"right":"left",e.textBaseline="bottom",e.fillText(b,y+r*3,T-1)}e.font=`${N(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,T),e.lineTo(l+f,T),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";for(const o of Ue()){const b=A(o,l,f);e.beginPath(),e.moveTo(b,T),e.lineTo(b,T+4),e.stroke(),e.fillText(ue.tick(o),b,T+6)}ue.unitLabel&&(e.textAlign="left",e.font=`${N(-1)}px system-ui, sans-serif`,e.fillText(`(${ue.unitLabel})`,l+f+22,T+6),e.font=`${N(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function Ce(t){const i=Math.floor((t-L+C/2)/(M+C));return i>=0&&i<c.length?i:-1}function P(t){return xe(t,z,"engineering")}const J=!!u&&u.length>0&&x===null,Se=t=>!J&&!!q&&g[t]?.waferIndex!==void 0;p.addEventListener("mousemove",t=>{const i=p.getBoundingClientRect(),s=Ce(t.clientY-i.top),$=s>=0&&c[s].count>0&&(J||Se(s));if(s!==W&&(W=s,p.style.cursor=$?"pointer":"default",K()),s>=0&&c[s].count>0){const e=c[s],l=J?`<br><em>click to see this ${k(ee)} by wafer</em>`:Se(s)?`<br><em>click to ${k(_e)}</em>`:"";R.innerHTML=`<strong>${k(e.label)}</strong> (${e.count} dies)<br>max ${k(P(e.max))}<br>q3 ${k(P(e.q3))}<br>median ${k(P(e.median))}<br>q1 ${k(P(e.q1))}<br>min ${k(P(e.min))}${l}`,R.style.display="block",lt(R,a,t.clientX,t.clientY)}else R.style.display="none"}),p.addEventListener("mouseleave",()=>{W!==-1&&(W=-1,K()),R.style.display="none"}),p.addEventListener("click",t=>{const i=p.getBoundingClientRect(),s=Ce(t.clientY-i.top);if(s===-1||c[s].count===0)return;if(J){const e=c[s].label;be(e),h.onGroupChange?.(e);return}const $=g[s]?.waferIndex;q&&$!==void 0&&v!==null&&q($,v)}),ae?.disconnect(),ae=ot(a,()=>K()),K(),ft(a,d,ye)}S();function qe(n){Te.set(n)&&(v=n,S())}function Ye(n){le.set(n)&&S()}return{card:a,setTest:qe,setAxisPrefs:Ye,destroy:()=>ae?.disconnect()}}
@@ -1 +1 @@
1
- import{SHADOW as I,LEADING as F,wireControlHover as L,controlStyle as T,SPACE as m,RADIUS as k,fontPx as j,FONT as v,CLR as f,Z_BASE as q,menuLayerFor as G,wireListNavigation as U,MENU_SEARCH_THRESHOLD as D,makeMenuSearchBox as Y,markMenuTrigger as R,saveImageBlob as X,openReparentedModal as Z}from"../toolbar.js";import{ICONS as N}from"../icons.js";import{fmt as K,fmtColorbarAxis as J}from"../../renderer/fmt.js";export function openChartExpandModal(e,r,n){for(const i of e.querySelectorAll('div[style*="position: absolute"]'))i.style.display==="block"&&i.style.pointerEvents==="none"&&(i.style.display="none");const t=e.getAttribute("style")??"";if(Object.assign(e.style,{flex:"1",minHeight:"0",border:"none",borderRadius:"0"}),!Z([e],{title:r,onClosed:()=>{e.setAttribute("style",t),n&&(n.style.display="flex")}})){e.setAttribute("style",t);return}n&&(n.style.display="none")}const Q={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 r=getComputedStyle(e),n={};for(const t of Object.keys(P)){const o=r.getPropertyValue(`--wmap-${P[t]}`).trim();n[t]=o||Q[t]}return n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,r){const n=e.ownerDocument.defaultView??window;let t=0;const o=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,r()}))});return o.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,o.disconnect()}}}export function growCardToFitContent(e,r,n){const o=`${e.offsetHeight-r.clientHeight+n}px`;e.style.minHeight!==o&&(e.style.minHeight=o)}export function chartFillHeight(e,r,n,t){const o=e.ownerDocument.defaultView??window;let i=0;for(const a of Array.from(r.children)){if(a===n)continue;const s=a,l=o.getComputedStyle(s);i+=s.offsetHeight+(parseFloat(l.marginTop)||0)+(parseFloat(l.marginBottom)||0)}return growCardToFitContent(e,r,i+t),Math.max(t,r.clientHeight-i)}export function chartDpr(e){return(e.ownerDocument.defaultView??window).devicePixelRatio||1}export function prepareCanvas(e,r,n,t){const o=chartDpr(e);e.width=Math.max(1,Math.floor(n*o)),e.height=Math.max(1,Math.floor(t*o)),e.style.width=`${n}px`,e.style.height=`${t}px`;const i=e.getContext("2d");return i?(i.setTransform(o,0,0,o,0,0),i.clearRect(0,0,n,t),i.font=`${j(-1)}px system-ui, sans-serif`,i.textBaseline="middle",{ctx:i,theme:resolveChartCanvasColors(r)}):null}export function applyCanvasFlow(e,r=0){let n;if(typeof r=="number")n=r;else{const t=(r.ownerDocument.defaultView??window).getComputedStyle(r);n=r.offsetTop+r.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}`:K(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,r){if(!r)return{tick:formatValue,unitLabel:""};const{tickFmt:n,axisLabel:t}=J(e,null,r);return{tick:n,unitLabel:t}}export function drawAxisUnit(e,r,n,t,o){const i={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font=`${j(-1)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="middle",e.fillStyle=o,e.fillText(`(${r})`,n,t),Object.assign(e,i)}export function saveCanvasPng(e,r,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const o=t.getContext("2d");o.fillStyle="#ffffff",o.fillRect(0,0,t.width,t.height),o.drawImage(e,0,0),t.toBlob(i=>{i&&X(i,r,n)},"image/png")}const z="10px";export function chartSwatchCss(e,r="solid"){return["display:inline-block",`width:${z}`,`height:${z}`,`border-radius:${k.pill}`,`background:${r==="outline"?"transparent":e}`,`border:1px ${r==="outline"?"dashed":"solid"} ${e}`,"box-sizing:border-box","flex-shrink:0"].join(";")}const ee="0.35";export function makeSeriesLegendItem(e,r,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:k.pill,border:`1px solid ${f.menuBorder}`,background:"none",cursor:"pointer",fontSize:v.body,color:f.text,whiteSpace:"nowrap"}),L(t,"bare");const o=n.createElement("span");return o.style.cssText=chartSwatchCss(r),t.append(o,n.createTextNode(e)),{el:t,setState({selected:i,dimmed:a}){t.style.borderColor=i?f.iconActive:f.menuBorder,t.style.fontWeight=i?"600":"400",t.style.opacity=a?ee:"1",t.setAttribute("aria-pressed",i?"true":"false")}}}export function cardFrameStyle(){return{background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:k.container,padding:m.xl,fontVariantNumeric:"tabular-nums"}}export function cardShell(e,r,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 o=t.ownerDocument.createElement("div");Object.assign(o.style,{display:"flex",alignItems:"center",gap:m.sm,paddingBottom:"6px",marginBottom:m.md,borderBottom:`1px solid ${f.menuBorder}`});const i=t.ownerDocument.createElement("div");i.textContent=e,Object.assign(i.style,{color:f.value,fontSize:v.heading,fontWeight:"600",flex:"1"}),o.appendChild(i);const a=makeTooltip(t),s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Save as PNG"),s.innerHTML=N.download,Object.assign(s.style,{...T("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:F.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(s),attachChartTip(s,t,a,"Save as PNG"),s.addEventListener("click",()=>{const p=t.querySelector("canvas");p&&saveCanvasPng(p,e,r)}),o.appendChild(s);const l=t.ownerDocument.createElement("button");l.type="button",l.setAttribute("aria-label","Expand"),l.innerHTML=N.expand,Object.assign(l.style,{...T("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:F.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(l),attachChartTip(l,t,a,"Expand"),l.addEventListener("click",()=>openChartExpandModal(t,i.textContent??e,l)),o.appendChild(l),t.appendChild(o);const c=t.ownerDocument.createElement("div");Object.assign(c.style,{display:"flex",gap:m.sm,marginBottom:m.md,flexWrap:"wrap",alignItems:"center"}),t.appendChild(c);const d=t.ownerDocument.createElement("div");return Object.assign(d.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(d),{card:t,heading:i,controlsRow:c,body:d}}export function makeSegmented(e,r,n,t=document,o=!1){const i=t.createElement("div");i.setAttribute("role","radiogroup"),Object.assign(i.style,{display:"inline-flex",border:`1px solid ${f.menuBorder}`,borderRadius:k.control,overflow:"hidden"});const a=`seg-${Math.random().toString(36).slice(2,9)}`,s=[];return e.forEach(([l,c],d)=>{const p=t.createElement("label");Object.assign(p.style,{display:"inline-flex",alignItems:"center",fontSize:v.body,padding:o?"1px 6px":"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:d>0?`1px solid ${f.menuBorder}`:"none"}),L(p,"bare");const u=t.createElement("input");u.type="radio",u.name=a,u.value=l,u.checked=l===r,Object.assign(u.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const w=()=>{p.style.background=u.checked?f.bgActive:f.menuBg,p.style.color=u.checked?f.iconActive:f.text,p.dataset.on=u.checked?"true":"false"};s.push(w),w(),u.addEventListener("change",()=>{if(u.checked){for(const C of s)C();n(l)}}),p.append(u,t.createTextNode(c)),i.appendChild(p)}),i}export function makeBackButton(e,r=document){const n=r.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{...T("outlined"),background:f.menuBg,color:f.label}),L(n),n.addEventListener("click",e),n}export function renderEmptyState(e,r,n){const t=e.ownerDocument.createElement("div");t.textContent=r,Object.assign(t.style,{color:f.label,fontSize:v.body,padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeListSelect(e,r,n,t={}){const{maxWidth:o="200px",ownerDocument:i=document,ariaLabel:a,emptyText:s,searchPlaceholder:l="Filter\u2026",hook:c}=t;let d=r;const p=x=>e.find(O=>O.value===x)?.label??"",u=i.createElement("button");u.type="button",c&&(u.dataset.wmapSelect=c),a&&u.setAttribute("aria-label",a),Object.assign(u.style,{fontSize:v.body,padding:`${m.xxs} ${m.sm}`,background:f.menuBg,color:f.text,border:`1px solid ${f.menuBorder}`,borderRadius:k.control,maxWidth:o,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:m.sm,justifyContent:"space-between"}),L(u),R(u,!1);const w=i.createElement("span");Object.assign(w.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const C=i.createElement("span");C.textContent="\u25BE",C.style.flex="0 0 auto",u.append(w,C);const S=()=>{w.textContent=e.length===0?s??"(none)":p(d)||s||"(none)"};if(S(),e.length===0)return u.disabled=!0,u.style.opacity="0.6",u.style.cursor="default",Object.defineProperty(u,"value",{get:()=>d,set:x=>{d=x,S()}});let h=null;const E=()=>{h&&(h.remove(),h=null,R(u,!1))},A=()=>{E(),u.focus()};function W(){const x=u.getBoundingClientRect(),O=i.defaultView??window,H=Math.max(x.width,220),_=Math.min(x.left,Math.max(4,(O.innerWidth??1/0)-H-4));h=i.createElement("div"),h.setAttribute("role","listbox"),h.setAttribute("aria-label",a??"Options"),Object.assign(h.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${_}px`,background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:k.control,boxShadow:I.menu,zIndex:q,minWidth:`${H}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const $=[],B=()=>$.filter(b=>b.row.style.display!=="none").map(b=>b.row);if(e.length>D){const b=Y(y=>{for(const g of $)g.row.style.display=g.label.includes(y)?"":"none"},l,i);Object.assign(b.style,{position:"sticky",top:"0",zIndex:"1",background:f.menuBg}),b.addEventListener("keydown",y=>{if(y.key==="Escape"){A();return}y.key==="ArrowDown"&&(y.preventDefault(),B()[0]?.focus())}),h.appendChild(b)}for(const b of e){const y=b.value===d,g=i.createElement("div");g.textContent=b.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:v.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",V=>{V.stopPropagation(),d=b.value,S(),A(),n(b.value)}),h.appendChild(g),$.push({row:g,label:b.label.toLowerCase()})}if(U(h,B,A),G(u).appendChild(h),R(u,!0),e.length<=D){const b=e.findIndex(y=>y.value===d);B()[b>=0?b:0]?.focus()}}u.addEventListener("click",x=>{if(x.stopPropagation(),h){E();return}W()});const M=x=>{if(!u.isConnected){i.removeEventListener("click",M);return}h&&!h.contains(x.target)&&!u.contains(x.target)&&E()};return i.addEventListener("click",M),Object.defineProperty(u,"value",{get:()=>d,set:x=>{d=x,S()}})}export function makeTestSelect(e,r,n,t={}){const{maxWidth:o="200px",emptyText:i="No parametric tests",ownerDocument:a=document}=t;return makeListSelect(e.map(s=>({value:String(s.testNumber),label:s.name||`Test ${s.testNumber}`})),r!==null?String(r):"",s=>n(Number(s)),{maxWidth:o,ownerDocument:a,ariaLabel:"Test",emptyText:i,searchPlaceholder:"Filter tests\u2026"})}export function makeLinkedAxisPrefs(e,r,n,t=document){let o=r?.includeLimits,i=r?.clipOutliers??!1;const a=()=>n({includeLimits:o,clipOutliers:i});return{get:()=>({includeLimits:o,clipOutliers:i}),sync(s,l){e.innerHTML="",l&&e.appendChild(makeToggle("Axis includes limits",s,c=>{o=c,a()},t)),e.appendChild(makeToggle("Clip outliers",i,c=>{i=c,a()},t))},set(s){return s.includeLimits===o&&s.clipOutliers===i?!1:(o=s.includeLimits,i=s.clipOutliers,!0)}}}export function makeLinkedTestSelect(e,r,n,t={}){let o=r;const i=makeTestSelect(e,o,a=>{a!==o&&(o=a,n(a))},t);return{el:i,get:()=>o,set(a){return o===a||!e.some(s=>s.testNumber===a)?!1:(o=a,i.value=String(a),!0)}}}export const ALL_GROUPS="\0all";export function makeLinkedGroupSelect(e,r,n,t,o={}){let i=n;const a=d=>d??ALL_GROUPS,s=d=>d===ALL_GROUPS?null:d,l=makeLabeledSelect(r,[{value:ALL_GROUPS,label:"All groups"},...e.map(d=>({value:d,label:d}))],a(i),d=>{const p=s(d);p!==i&&(i=p,t(p))},{...o,hook:"group-scope"}),c=l.querySelector('[data-wmap-select="group-scope"]');return{el:l,get:()=>i,set(d){return i===d||d!==null&&!e.includes(d)?!1:(i=d,c&&(c.value=a(d)),!0)}}}export function makeWaferSelect(e,r,n,t={}){const{maxWidth:o="160px",allLabel:i="All wafers",ownerDocument:a=document}=t,s="\0all";return makeListSelect([{value:s,label:i},...e.map((l,c)=>({value:String(c),label:l.label??`#${c}`}))],r===null?s:String(r),l=>n(l===s?null:Number(l)),{maxWidth:o,ownerDocument:a,ariaLabel:i,searchPlaceholder:"Filter wafers\u2026"})}export function makeToggle(e,r,n,t=document){const o=t.createElement("label");Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:m.xs,fontSize:v.body,color:f.label,cursor:"pointer",userSelect:"none"}),L(o,"bare");const i=t.createElement("input");return i.type="checkbox",i.checked=r,i.style.cssText="margin:0;cursor:pointer;",i.addEventListener("change",()=>n(i.checked)),o.append(i,t.createTextNode(e)),o}export function makeLabeledSelect(e,r,n,t,o={}){const{maxWidth:i="160px",hook:a,ownerDocument:s=document}=o,l=s.createElement("label");l.textContent=e,Object.assign(l.style,{color:f.label,fontSize:v.body,display:"flex",alignItems:"center",gap:m.xs});const c=makeListSelect(r,n,t,{maxWidth:i,ownerDocument:s,hook:a,ariaLabel:e.replace(/:\s*$/,"")});return l.appendChild(c),l}export function makeChartGridWrap(e=document){const r=e.createElement("div");return r.dataset.wmapChartGrid="1",Object.assign(r.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:m.lg,flex:"0 0 auto"}),r}export function makeTooltip(e){const r=e.ownerDocument.createElement("div");return Object.assign(r.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:k.control,padding:`${m.sm} ${m.xl}`,fontSize:v.sub,fontFamily:v.family,maxWidth:"280px",whiteSpace:"nowrap",boxShadow:I.menu}),e.appendChild(r),r}export function attachChartTip(e,r,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",o=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,r,o.clientX,o.clientY)}),e.addEventListener("mousemove",o=>{n.style.display==="block"&&positionChartTooltip(n,r,o.clientX,o.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 o=e.getBoundingClientRect();positionChartTooltip(n,r,o.left+o.width/2,o.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,r,n,t){const o=r.getBoundingClientRect(),i=14,a=4;e.style.left="0",e.style.top="0";const s=e.offsetWidth,l=e.offsetHeight;let c=n-o.left+i,d=t-o.top+i;c+s+a>o.width&&(c=n-o.left-i-s),d+l+a>o.height&&(d=t-o.top-i-l),c=Math.max(a,c),d=Math.max(a,d),e.style.left=`${c}px`,e.style.top=`${d}px`}export function robustFence(e,r=1.5){const n=e.filter(s=>Number.isFinite(s)).sort((s,l)=>s-l);if(n.length<8)return null;const t=s=>{const l=(n.length-1)*s,c=Math.floor(l),d=Math.ceil(l);return c===d?n[c]:n[c]+(n[d]-n[c])*(l-c)},o=t(.25),i=t(.75),a=i-o;return a===0?null:{lo:o-r*a,hi:i+r*a}}export function shouldIncludeLimitsByDefault(e,r,n,t,o=1/3){if(n===void 0&&t===void 0||!Number.isFinite(e)||!Number.isFinite(r))return!1;const i=Math.min(e,n??e),s=Math.max(r,t??r)-i;if(s<=0)return!0;const l=r-e;return l===0?!0:l/s>=o}export function resolveAxisRange(e){const{dataMin:r,dataMax:n,limitLow:t,limitHigh:o,includeLimits:i,clipOutliers:a,values:s}=e;let l=r,c=n,d=0;if(a&&s?.length){const u=robustFence(s);if(u){const w=Math.max(l,u.lo),C=Math.min(c,u.hi);C>w&&(d=s.filter(S=>Number.isFinite(S)&&(S<w||S>C)).length,l=w,c=C)}}i&&(t!==void 0&&(l=Math.min(l,t)),o!==void 0&&(c=Math.max(c,o))),l===c&&(l-=1,c+=1);const p=[];return t!==void 0&&t<l&&p.push({value:t,label:"LSL",side:"lo"}),t!==void 0&&t>c&&p.push({value:t,label:"LSL",side:"hi"}),o!==void 0&&o>c&&p.push({value:o,label:"USL",side:"hi"}),o!==void 0&&o<l&&p.push({value:o,label:"USL",side:"lo"}),{lo:l,hi:c,offAxis:p,clippedCount:d}}export function limitLabelSide(e,r,n,t,o,i=3){const a=o?-1:1;return(a<0?e-i-r>=n:e+i+r<=t)?a:-a}export function drawOffAxisLimits(e,r,n,t,o,i){if(r.length){e.save(),e.fillStyle=o,e.font=`${j(-1)}px system-ui, sans-serif`;for(const{value:a,label:s,side:l}of r){const c=t==="horizontal"?l==="lo"?"\u2190":"\u2192":l==="lo"?"\u2193":"\u2191",d=l==="lo"?`${c} ${s} ${i(a)}`:`${s} ${i(a)} ${c}`;t==="horizontal"?(e.textBaseline="top",e.textAlign=l==="lo"?"left":"right",e.fillText(d,l==="lo"?n.left+2:n.right-2,n.top+2)):(e.textBaseline=l==="lo"?"bottom":"top",e.textAlign="left",e.fillText(d,n.left+4,l==="lo"?n.bottom-2:n.top+2))}e.restore()}}
1
+ import{SHADOW as I,LEADING as F,wireControlHover as L,controlStyle as T,SPACE as m,RADIUS as k,fontPx as j,FONT as v,CLR as f,Z_BASE as q,menuLayerFor as G,wireListNavigation as U,MENU_SEARCH_THRESHOLD as D,makeMenuSearchBox as Y,markMenuTrigger as R,saveImageBlob as X,openReparentedModal as Z}from"../toolbar.js";import{ICONS as N}from"../icons.js";import{fmt as K,fmtColorbarAxis as J}from"../../renderer/fmt.js";export function openChartExpandModal(e,r,n){for(const i of e.querySelectorAll('div[style*="position: absolute"]'))i.style.display==="block"&&i.style.pointerEvents==="none"&&(i.style.display="none");const t=e.getAttribute("style")??"";if(Object.assign(e.style,{flex:"1",minHeight:"0",border:"none",borderRadius:"0"}),!Z([e],{title:r,onClosed:()=>{e.setAttribute("style",t),n&&(n.style.display="flex")}})){e.setAttribute("style",t);return}n&&(n.style.display="none")}const Q={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 r=getComputedStyle(e),n={};for(const t of Object.keys(P)){const o=r.getPropertyValue(`--wmap-${P[t]}`).trim();n[t]=o||Q[t]}return n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,r){const n=e.ownerDocument.defaultView??window;let t=0;const o=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,r()}))});return o.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,o.disconnect()}}}export function growCardToFitContent(e,r,n){const o=`${e.offsetHeight-r.clientHeight+n}px`;e.style.minHeight!==o&&(e.style.minHeight=o)}export function chartFillHeight(e,r,n,t){const o=e.ownerDocument.defaultView??window;let i=0;for(const a of Array.from(r.children)){if(a===n)continue;const s=a,l=o.getComputedStyle(s);i+=s.offsetHeight+(parseFloat(l.marginTop)||0)+(parseFloat(l.marginBottom)||0)}return growCardToFitContent(e,r,i+t),Math.max(t,r.clientHeight-i)}export function chartDpr(e){return(e.ownerDocument.defaultView??window).devicePixelRatio||1}export function prepareCanvas(e,r,n,t){const o=chartDpr(e);e.width=Math.max(1,Math.floor(n*o)),e.height=Math.max(1,Math.floor(t*o)),e.style.width=`${n}px`,e.style.height=`${t}px`;const i=e.getContext("2d");return i?(i.setTransform(o,0,0,o,0,0),i.clearRect(0,0,n,t),i.font=`${j(-1)}px system-ui, sans-serif`,i.textBaseline="middle",{ctx:i,theme:resolveChartCanvasColors(r)}):null}export function applyCanvasFlow(e,r=0){let n;if(typeof r=="number")n=r;else{const t=(r.ownerDocument.defaultView??window).getComputedStyle(r);n=r.offsetTop+r.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}`:K(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,r){if(!r)return{tick:formatValue,unitLabel:""};const{tickFmt:n,axisLabel:t}=J(e,null,r);return{tick:n,unitLabel:t}}export function drawAxisUnit(e,r,n,t,o){const i={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font=`${j(-1)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="middle",e.fillStyle=o,e.fillText(`(${r})`,n,t),Object.assign(e,i)}export function saveCanvasPng(e,r,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const o=t.getContext("2d");o.fillStyle="#ffffff",o.fillRect(0,0,t.width,t.height),o.drawImage(e,0,0),t.toBlob(i=>{i&&X(i,r,n)},"image/png")}const z="10px";export function chartSwatchCss(e,r="solid"){return["display:inline-block",`width:${z}`,`height:${z}`,`border-radius:${k.pill}`,`background:${r==="outline"?"transparent":e}`,`border:1px ${r==="outline"?"dashed":"solid"} ${e}`,"box-sizing:border-box","flex-shrink:0"].join(";")}const ee="0.35";export function makeSeriesLegendItem(e,r,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:k.pill,border:`1px solid ${f.menuBorder}`,background:"none",cursor:"pointer",fontSize:v.body,color:f.text,whiteSpace:"nowrap"}),L(t,"bare");const o=n.createElement("span");return o.style.cssText=chartSwatchCss(r),t.append(o,n.createTextNode(e)),{el:t,setState({selected:i,dimmed:a}){t.style.borderColor=i?f.iconActive:f.menuBorder,t.style.fontWeight=i?"600":"400",t.style.opacity=a?ee:"1",t.setAttribute("aria-pressed",i?"true":"false")}}}export function cardFrameStyle(){return{background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:k.container,padding:m.xl,fontVariantNumeric:"tabular-nums"}}export function cardShell(e,r,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 o=t.ownerDocument.createElement("div");Object.assign(o.style,{display:"flex",alignItems:"center",gap:m.sm,paddingBottom:"6px",marginBottom:m.md,borderBottom:`1px solid ${f.menuBorder}`});const i=t.ownerDocument.createElement("div");i.textContent=e,Object.assign(i.style,{color:f.value,fontSize:v.heading,fontWeight:"600",flex:"1"}),o.appendChild(i);const a=makeTooltip(t),s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Save as PNG"),s.innerHTML=N.download,Object.assign(s.style,{...T("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:F.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(s),attachChartTip(s,t,a,"Save as PNG"),s.addEventListener("click",()=>{const p=t.querySelector("canvas");p&&saveCanvasPng(p,e,r)}),o.appendChild(s);const l=t.ownerDocument.createElement("button");l.type="button",l.setAttribute("aria-label","Expand"),l.innerHTML=N.expand,Object.assign(l.style,{...T("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:F.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(l),attachChartTip(l,t,a,"Expand"),l.addEventListener("click",()=>openChartExpandModal(t,i.textContent??e,l)),o.appendChild(l),t.appendChild(o);const c=t.ownerDocument.createElement("div");Object.assign(c.style,{display:"flex",gap:m.sm,marginBottom:m.md,flexWrap:"wrap",alignItems:"center"}),t.appendChild(c);const d=t.ownerDocument.createElement("div");return Object.assign(d.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(d),{card:t,heading:i,controlsRow:c,body:d}}export function makeSegmented(e,r,n,t=document,o=!1){const i=t.createElement("div");i.setAttribute("role","radiogroup"),Object.assign(i.style,{display:"inline-flex",border:`1px solid ${f.menuBorder}`,borderRadius:k.control,overflow:"hidden"});const a=`seg-${Math.random().toString(36).slice(2,9)}`,s=[];return e.forEach(([l,c],d)=>{const p=t.createElement("label");Object.assign(p.style,{display:"inline-flex",alignItems:"center",fontSize:v.body,padding:o?"1px 6px":"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:d>0?`1px solid ${f.menuBorder}`:"none"}),L(p,"bare");const u=t.createElement("input");u.type="radio",u.name=a,u.value=l,u.checked=l===r,Object.assign(u.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const w=()=>{p.style.background=u.checked?f.bgActive:f.menuBg,p.style.color=u.checked?f.iconActive:f.text,p.dataset.on=u.checked?"true":"false"};s.push(w),w(),u.addEventListener("change",()=>{if(u.checked){for(const C of s)C();n(l)}}),p.append(u,t.createTextNode(c)),i.appendChild(p)}),i}export function makeBackButton(e,r=document){const n=r.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{...T("outlined"),background:f.menuBg,color:f.label}),L(n),n.addEventListener("click",e),n}export function renderEmptyState(e,r,n){const t=e.ownerDocument.createElement("div");t.textContent=r,Object.assign(t.style,{color:f.label,fontSize:v.body,padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeListSelect(e,r,n,t={}){const{maxWidth:o="200px",ownerDocument:i=document,ariaLabel:a,emptyText:s,searchPlaceholder:l="Filter\u2026",hook:c}=t;let d=r;const p=x=>e.find(O=>O.value===x)?.label??"",u=i.createElement("button");u.type="button",c&&(u.dataset.wmapSelect=c),a&&u.setAttribute("aria-label",a),Object.assign(u.style,{fontSize:v.body,padding:`${m.xxs} ${m.sm}`,background:f.menuBg,color:f.text,border:`1px solid ${f.menuBorder}`,borderRadius:k.control,maxWidth:o,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:m.sm,justifyContent:"space-between"}),L(u),R(u,!1);const w=i.createElement("span");Object.assign(w.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const C=i.createElement("span");C.textContent="\u25BE",C.style.flex="0 0 auto",u.append(w,C);const S=()=>{w.textContent=e.length===0?s??"(none)":p(d)||s||"(none)"};if(S(),e.length===0)return u.disabled=!0,u.style.opacity="0.6",u.style.cursor="default",Object.defineProperty(u,"value",{get:()=>d,set:x=>{d=x,S()}});let h=null;const E=()=>{h&&(h.remove(),h=null,R(u,!1))},A=()=>{E(),u.focus()};function W(){const x=u.getBoundingClientRect(),O=i.defaultView??window,H=Math.max(x.width,220),_=Math.min(x.left,Math.max(4,(O.innerWidth??1/0)-H-4));h=i.createElement("div"),h.setAttribute("role","listbox"),h.setAttribute("aria-label",a??"Options"),Object.assign(h.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${_}px`,background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:k.control,boxShadow:I.menu,zIndex:q,minWidth:`${H}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const $=[],B=()=>$.filter(b=>b.row.style.display!=="none").map(b=>b.row);if(e.length>D){const b=Y(y=>{for(const g of $)g.row.style.display=g.label.includes(y)?"":"none"},l,i);Object.assign(b.style,{position:"sticky",top:"0",zIndex:"1",background:f.menuBg}),b.addEventListener("keydown",y=>{if(y.key==="Escape"){A();return}y.key==="ArrowDown"&&(y.preventDefault(),B()[0]?.focus())}),h.appendChild(b)}for(const b of e){const y=b.value===d,g=i.createElement("div");g.textContent=b.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:v.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",V=>{V.stopPropagation(),d=b.value,S(),A(),n(b.value)}),h.appendChild(g),$.push({row:g,label:b.label.toLowerCase()})}if(U(h,B,A),G(u).appendChild(h),R(u,!0),e.length<=D){const b=e.findIndex(y=>y.value===d);B()[b>=0?b:0]?.focus()}}u.addEventListener("click",x=>{if(x.stopPropagation(),h){E();return}W()});const M=x=>{if(!u.isConnected){i.removeEventListener("click",M);return}h&&!h.contains(x.target)&&!u.contains(x.target)&&E()};return i.addEventListener("click",M),Object.defineProperty(u,"value",{get:()=>d,set:x=>{d=x,S()}})}export function makeTestSelect(e,r,n,t={}){const{maxWidth:o="200px",emptyText:i="No parametric tests",ownerDocument:a=document}=t;return makeListSelect(e.map(s=>({value:String(s.testNumber),label:s.name||`Test ${s.testNumber}`})),r!==null?String(r):"",s=>n(Number(s)),{maxWidth:o,ownerDocument:a,ariaLabel:"Test",emptyText:i,searchPlaceholder:"Filter tests\u2026"})}export function makeLinkedAxisPrefs(e,r,n,t=document){let o=r?.includeLimits,i=r?.clipOutliers??!1;const a=()=>n({includeLimits:o,clipOutliers:i});return{get:()=>({includeLimits:o,clipOutliers:i}),sync(s,l){e.innerHTML="",l&&e.appendChild(makeToggle("Axis includes limits",s,c=>{o=c,a()},t)),e.appendChild(makeToggle("Clip outliers",i,c=>{i=c,a()},t))},set(s){return s.includeLimits===o&&s.clipOutliers===i?!1:(o=s.includeLimits,i=s.clipOutliers,!0)}}}export function makeLinkedTestSelect(e,r,n,t={}){let o=r;const i=makeTestSelect(e,o,a=>{a!==o&&(o=a,n(a))},t);return{el:i,get:()=>o,set(a){return o===a||!e.some(s=>s.testNumber===a)?!1:(o=a,i.value=String(a),!0)}}}export const ALL_GROUPS="\0all";export function makeLinkedGroupSelect(e,r,n,t,o={}){let i=n;const a=d=>d??ALL_GROUPS,s=d=>d===ALL_GROUPS?null:d,l=makeLabeledSelect(r,[{value:ALL_GROUPS,label:"All groups"},...e.map(d=>({value:d,label:d}))],a(i),d=>{const p=s(d);p!==i&&(i=p,t(p))},{...o,hook:"group-scope"}),c=l.querySelector('[data-wmap-select="group-scope"]');return{el:l,get:()=>i,set(d){return i===d||d!==null&&!e.includes(d)?!1:(i=d,c&&(c.value=a(d)),!0)}}}export function makeWaferSelect(e,r,n,t={}){const{maxWidth:o="160px",allLabel:i="All wafers",ownerDocument:a=document}=t,s="\0all";return makeListSelect([{value:s,label:i},...e.map((l,c)=>({value:String(c),label:l.label??`#${c}`}))],r===null?s:String(r),l=>n(l===s?null:Number(l)),{maxWidth:o,ownerDocument:a,ariaLabel:i,searchPlaceholder:"Filter wafers\u2026"})}export function makeToggle(e,r,n,t=document){const o=t.createElement("label");Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:m.xs,fontSize:v.body,color:f.label,cursor:"pointer",userSelect:"none"}),L(o,"bare");const i=t.createElement("input");return i.type="checkbox",i.checked=r,i.style.cssText="margin:0;cursor:pointer;",i.addEventListener("change",()=>n(i.checked)),o.append(i,t.createTextNode(e)),o}export function makeLabeledSelect(e,r,n,t,o={}){const{maxWidth:i="160px",hook:a,ownerDocument:s=document}=o,l=s.createElement("label");l.textContent=e,Object.assign(l.style,{color:f.label,fontSize:v.body,display:"flex",alignItems:"center",gap:m.xs});const c=makeListSelect(r,n,t,{maxWidth:i,ownerDocument:s,hook:a,ariaLabel:e.replace(/:\s*$/,"")});return l.appendChild(c),l}export function makeChartGridWrap(e=document){const r=e.createElement("div");return r.dataset.wmapChartGrid="1",Object.assign(r.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:m.lg,flex:"0 0 auto"}),r}export function makeTooltip(e){const r=e.ownerDocument.createElement("div");return Object.assign(r.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:k.control,padding:`${m.sm} ${m.xl}`,fontSize:v.sub,fontFamily:v.family,maxWidth:"280px",whiteSpace:"nowrap",boxShadow:I.menu}),e.appendChild(r),r}export function attachChartTip(e,r,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",o=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,r,o.clientX,o.clientY)}),e.addEventListener("mousemove",o=>{n.style.display==="block"&&positionChartTooltip(n,r,o.clientX,o.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 o=e.getBoundingClientRect();positionChartTooltip(n,r,o.left+o.width/2,o.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,r,n,t){const o=r.getBoundingClientRect(),i=14,a=4;e.style.left="0",e.style.top="0";const s=e.offsetWidth,l=e.offsetHeight;let c=n-o.left+i,d=t-o.top+i;c+s+a>o.width&&(c=n-o.left-i-s),d+l+a>o.height&&(d=t-o.top-i-l),c=Math.max(a,c),d=Math.max(a,d),e.style.left=`${c}px`,e.style.top=`${d}px`}export function robustFence(e,r=1.5){const n=Float64Array.from(e.filter(s=>Number.isFinite(s)));if(n.sort(),n.length<8)return null;const t=s=>{const l=(n.length-1)*s,c=Math.floor(l),d=Math.ceil(l);return c===d?n[c]:n[c]+(n[d]-n[c])*(l-c)},o=t(.25),i=t(.75),a=i-o;return a===0?null:{lo:o-r*a,hi:i+r*a}}export function shouldIncludeLimitsByDefault(e,r,n,t,o=1/3){if(n===void 0&&t===void 0||!Number.isFinite(e)||!Number.isFinite(r))return!1;const i=Math.min(e,n??e),s=Math.max(r,t??r)-i;if(s<=0)return!0;const l=r-e;return l===0?!0:l/s>=o}export function resolveAxisRange(e){const{dataMin:r,dataMax:n,limitLow:t,limitHigh:o,includeLimits:i,clipOutliers:a,values:s}=e;let l=r,c=n,d=0;if(a&&s?.length){const u=robustFence(s);if(u){const w=Math.max(l,u.lo),C=Math.min(c,u.hi);C>w&&(d=s.filter(S=>Number.isFinite(S)&&(S<w||S>C)).length,l=w,c=C)}}i&&(t!==void 0&&(l=Math.min(l,t)),o!==void 0&&(c=Math.max(c,o))),l===c&&(l-=1,c+=1);const p=[];return t!==void 0&&t<l&&p.push({value:t,label:"LSL",side:"lo"}),t!==void 0&&t>c&&p.push({value:t,label:"LSL",side:"hi"}),o!==void 0&&o>c&&p.push({value:o,label:"USL",side:"hi"}),o!==void 0&&o<l&&p.push({value:o,label:"USL",side:"lo"}),{lo:l,hi:c,offAxis:p,clippedCount:d}}export function limitLabelSide(e,r,n,t,o,i=3){const a=o?-1:1;return(a<0?e-i-r>=n:e+i+r<=t)?a:-a}export function drawOffAxisLimits(e,r,n,t,o,i){if(r.length){e.save(),e.fillStyle=o,e.font=`${j(-1)}px system-ui, sans-serif`;for(const{value:a,label:s,side:l}of r){const c=t==="horizontal"?l==="lo"?"\u2190":"\u2192":l==="lo"?"\u2193":"\u2191",d=l==="lo"?`${c} ${s} ${i(a)}`:`${s} ${i(a)} ${c}`;t==="horizontal"?(e.textBaseline="top",e.textAlign=l==="lo"?"left":"right",e.fillText(d,l==="lo"?n.left+2:n.right-2,n.top+2)):(e.textBaseline=l==="lo"?"bottom":"top",e.textAlign="left",e.fillText(d,n.left+4,l==="lo"?n.bottom-2:n.top+2))}e.restore()}}
@@ -1,2 +1,2 @@
1
- import{buildCorrelationMatrix as Mt,filterCorrelationMatrix as wt}from"../../stats/correlation.js";import{csvField as it}from"../../core/utils.js";import{CORRELATION_POSITIVE as at,CORRELATION_NEGATIVE as ct}from"./palette.js";import{buildFacetTable as vt}from"../../stats/facets.js";import{wireControlHover as Lt,controlStyle as Ct,SPACE as C,RADIUS as et,fontPx as dt,FONT as z,CLR as b,saveTextFile as It}from"../toolbar.js";import{attachChartTip as Tt,cardShell as St,observeResize as $t,makeTooltip as Et,positionChartTooltip as Rt,makeWaferSelect as Dt,renderEmptyState as kt,resolveChartCanvasColors as At,prepareCanvas as Bt,chartDpr as ut}from"./chartShell.js";import{escHtml as G}from"../../core/utils.js";const ft=5,pt=100,mt=20;function nt(x){const d=x.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(d)return[+d[1],+d[2],+d[3]];const f=x.trim().replace("#","");return f.length===6?[parseInt(f.slice(0,2),16),parseInt(f.slice(2,4),16),parseInt(f.slice(4,6),16)]:null}function Ot(x){const d=nt(x);return d?(d[0]*299+d[1]*587+d[2]*114)/1e3>140?"#111":"#fff":"#000"}function _t(x,d,f){const B=nt(x);if(!B)return x;const U=Math.round(d[0]+(B[0]-d[0])*f),O=Math.round(d[1]+(B[1]-d[1])*f),l=Math.round(d[2]+(B[2]-d[2])*f);return`rgb(${U},${O},${l})`}export function renderCorrelationPanel(x){const{title:d="Test correlation matrix",items:f,testDefs:B,onSaveImage:U,onSelectPair:O}=x,{card:l,body:D,controlsRow:q}=St(d,U,x.ownerDocument);D.style.overflowX="auto",l.style.alignSelf="start";let Y=mt,j=null;const H=l.ownerDocument.createElement("label");H.textContent="Max tests:",Object.assign(H.style,{color:b.label,fontSize:z.body,display:"flex",alignItems:"center",gap:C.xs});const L=l.ownerDocument.createElement("input");L.type="number",L.min=String(ft),L.max=String(pt),L.value=String(Y),Object.assign(L.style,{width:"52px",fontSize:z.body,padding:`${C.xxs} ${C.xs}`,background:b.menuBg,color:b.value,border:`1px solid ${b.menuBorder}`,borderRadius:et.control}),L.addEventListener("change",()=>{const e=Math.max(ft,Math.min(pt,parseInt(L.value,10)||mt));L.value=String(e),e!==Y&&(Y=e,K())}),H.appendChild(L),q.appendChild(H),f.length>1&&q.appendChild(Dt(f,j,e=>{j=e,K()},{ownerDocument:l.ownerDocument}));function ht(){return j!==null?f[j]?[f[j]]:[]:f}if(x.onSaveText){const e=l.ownerDocument.createElement("button");e.type="button",e.textContent="Correlation CSV",Object.assign(e.style,{...Ct("outlined"),color:b.text}),Lt(e),e.addEventListener("click",()=>{const o=ot;if(!o)return;const u=["Test X,Test X number,Test Y,Test Y number,r,n"];for(let r=0;r<o.tests.length;r++)for(let p=r+1;p<o.tests.length;p++){const i=o.cells.find(m=>m.xIndex===p&&m.yIndex===r);i&&u.push([it(o.tests[p].label),String(o.tests[p].testNumber),it(o.tests[r].label),String(o.tests[r].testNumber),i.r===null?"":i.r.toFixed(6),String(i.n)].join(","))}It(u.join(`
2
- `),"test-correlation.csv","text/csv",x.onSaveText)}),q.appendChild(e)}const _=l.ownerDocument.createElement("div");Object.assign(_.style,{display:"flex",flexDirection:"column",gap:C.xs,marginBottom:C.sm}),l.insertBefore(_,D);const I=Et(l);Tt(H,l,I,"Cap on how many tests the matrix includes (strongest correlations kept first)");let J=()=>{};function xt(e,o,u,r,p,i){if(_.innerHTML="",p.length>0){const w=l.ownerDocument.createElement("div");w.textContent=`\u26A0 Mixed ${p.join(", ")} within this set \u2014 correlations may be misleading (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,Object.assign(w.style,{color:b.warnText,background:b.warnBg,border:`1px solid ${b.warnBorder}`,borderRadius:et.control,padding:`${C.xs} ${C.md}`,fontSize:z.body}),_.appendChild(w)}const m=l.ownerDocument.createElement("span");Object.assign(m.style,{display:"inline-flex",alignItems:"center",gap:C.sm,color:b.label,fontSize:z.body,flexWrap:"wrap"});const g=l.ownerDocument.createElement("span");g.textContent=i!==null?`Pearson r \xB7 n \u2248 ${i.toLocaleString()} dies per pair \xB7 click a cell to view that pair in scatter \xB7`:"Pearson r \xB7 click a cell to view that pair in scatter \xB7",m.appendChild(g);const k=l.ownerDocument.createElement("span");Object.assign(k.style,{display:"inline-flex",alignItems:"center",gap:C.xs});const T=l.ownerDocument.createElement("span");T.textContent="\u22121";const N=l.ownerDocument.createElement("span");Object.assign(N.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:et.control,border:`1px solid ${b.menuBorder}`,background:`linear-gradient(to right, ${ct}, ${b.menuBg}, ${at})`});const a=l.ownerDocument.createElement("span");a.textContent="+1",k.append(T,N,a),m.appendChild(k),_.appendChild(m);const M=l.ownerDocument.createElement("span");if(Object.assign(M.style,{color:b.value,fontSize:z.body,fontWeight:"500"}),e===0&&o===0)M.textContent=r?`No significant correlations found \u2014 strongest pair: ${r.xLabel} \u2194 ${r.yLabel} (r = ${r.r.toFixed(2)})`:"No significant correlations found";else{const w=[];e>0&&w.push(`${e} strong (|r| \u2265 0.7)`),o>0&&w.push(`${o} moderate (0.4\u20130.7)`);const F=e+o;let X=w.join(", ")+` pair${F!==1?"s":""} found`;u>0&&(X+=` \xB7 ${u} weak pair${u!==1?"s":""} not shown`),M.textContent=X}_.appendChild(M)}function gt(e){if(D.innerHTML="",e.tests.length<2)return kt(D,"Need at least two parametric tests for a correlation matrix."),()=>{};const o=l.ownerDocument.createElement("canvas");o.style.display="block",o.style.cursor="default",D.appendChild(o);const u=e.tests.length,r=s=>s.label.split(" (#")[0],p=Math.min(14,Math.max(...e.tests.map(s=>r(s).length))),i=p*6.5+8,m=10,g=Math.round(m*6.5*Math.sin(Math.PI/4))+14,k=14,T=28,N=44;let a=-1,M=-1;function w(s){const c=Math.max(0,s-i);return Math.max(k,Math.min(N,Math.floor(c/u)))}const F=new Map;for(const s of e.cells){let c=F.get(s.yIndex);c||(c=[],F.set(s.yIndex,c)),c.push(s)}function X(){const s=At(l),c=nt(s.bg)??[255,255,255],h=D.clientWidth,n=w(h),W=n*u,S=g+n*u+4,$=i+W,E=Bt(o,l,$,S);if(!E)return;const{ctx:t}=E;t.font=`${dt(-1)}px system-ui, sans-serif`,t.fillStyle=s.text,e.tests.forEach((R,y)=>{const v=r(R),A=v.length>m?`${v.slice(0,m-1)}\u2026`:v,Q=i+y*n+n/2;t.save(),t.translate(Q,g-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=y===a?s.text:s.textMuted,t.fillText(A,0,0),t.restore()}),e.tests.forEach((R,y)=>{const v=r(R),A=g+y*n,Q=A+n/2;t.font=`${dt(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle",t.fillStyle=y===M?s.text:s.textMuted,t.fillText(v.length>p?`${v.slice(0,p-1)}\u2026`:v,i-4,Q),(F.get(y)??[]).forEach(lt=>{const Z=lt.xIndex,P=i+Z*n,yt=Z===a&&y===M,rt=Z===y,V=lt.r;if(V===null){t.fillStyle=s.border,t.globalAlpha=.4,t.fillRect(P+1,A+1,n-2,n-2),t.globalAlpha=1;return}let tt=s.bgHover;rt?t.fillStyle=s.bgHover:(tt=_t(V>=0?at:ct,c,Math.abs(V)),t.fillStyle=tt),t.fillRect(P+1,A+1,n-2,n-2),n>=T&&!rt&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Ot(tt),t.textAlign="center",t.textBaseline="middle",t.fillText(V.toFixed(2),P+n/2,A+n/2)),yt&&(t.strokeStyle=s.text,t.lineWidth=2,t.strokeRect(P+1,A+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=s.bg,t.lineWidth=1;for(let R=0;R<=u;R++){const y=i+R*n;t.beginPath(),t.moveTo(y,g),t.lineTo(y,g+u*n),t.stroke();const v=g+R*n;t.beginPath(),t.moveTo(i,v),t.lineTo(i+u*n,v),t.stroke()}}function st(s){const c=o.getBoundingClientRect(),h=D.clientWidth,n=w(h),W=(s.clientX-c.left)*(o.width/ut(o)/c.width),S=(s.clientY-c.top)*(o.height/ut(o)/c.height),$=Math.floor((W-i)/n),E=Math.floor((S-g)/n);return $>=0&&$<u&&E>=0&&E<u?{xi:$,yi:E}:null}return o.addEventListener("mousemove",s=>{const c=st(s);if(!c){I.style.display="none",o.style.cursor="default";return}const{xi:h,yi:n}=c,W=h===n;o.style.cursor=W||!O?"default":"pointer";const S=e.cells.find(t=>t.xIndex===h&&t.yIndex===n),$=e.tests[h].label,E=e.tests[n].label;W?I.innerHTML=`<strong>${G($)}</strong>`:S?.r!==null&&S?.r!==void 0?I.innerHTML=`<strong>${G(r(e.tests[n]))}</strong> (#${e.tests[n].testNumber}) vs <strong>${G(r(e.tests[h]))}</strong> (#${e.tests[h].testNumber})<br>r = ${S.r.toFixed(4)} \xB7 n = ${S.n.toLocaleString()}${O?"<br><em>click to view in scatter</em>":""}`:I.innerHTML=`${G(`${E} vs ${$}`)}<br><em>insufficient data</em>`,I.style.display="block",Rt(I,l,s.clientX,s.clientY)}),o.addEventListener("mouseleave",()=>{I.style.display="none"}),o.addEventListener("click",s=>{const c=st(s);if(!c||!O)return;const{xi:h,yi:n}=c;h!==n&&(a=h,M=n,X(),O(e.tests[h].testNumber,e.tests[n].testNumber))}),X}let ot=null;function K(){const e=ht(),o=e.flatMap(a=>a.dies??[]),u=Mt(o,B),{matrix:r,strongPairs:p,moderatePairs:i,hiddenWeakPairs:m,strongestPair:g}=wt(u,{minTests:6,maxTests:Y}),k=vt(e,{facetableOnly:!0}).filter(a=>a.splittable).map(a=>a.label),T=r.cells.filter(a=>a.xIndex!==a.yIndex).map(a=>a.n).sort((a,M)=>a-M),N=T.length?T[Math.floor(T.length/2)]:null;ot=r,xt(p,i,m,g,k,N),J=gt(r),J()}const bt=$t(l,()=>J());return K(),{card:l,destroy:()=>bt.disconnect()}}
1
+ import{buildCorrelationMatrix as wt,filterCorrelationMatrix as Mt,correlationSampleNote as vt}from"../../stats/correlation.js";import{csvField as it,maxOf as Ct}from"../../core/utils.js";import{CORRELATION_POSITIVE as at,CORRELATION_NEGATIVE as ct}from"./palette.js";import{buildFacetTable as Lt}from"../../stats/facets.js";import{wireControlHover as St,controlStyle as It,SPACE as C,RADIUS as nt,fontPx as dt,FONT as H,CLR as x,saveTextFile as Tt}from"../toolbar.js";import{attachChartTip as $t,cardShell as Et,observeResize as Dt,makeTooltip as Rt,positionChartTooltip as kt,makeWaferSelect as At,renderEmptyState as Bt,resolveChartCanvasColors as Ot,prepareCanvas as Nt,chartDpr as ut}from"./chartShell.js";import{escHtml as U}from"../../core/utils.js";const ft=5,pt=100,mt=20;function ot(b){const d=b.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(d)return[+d[1],+d[2],+d[3]];const f=b.trim().replace("#","");return f.length===6?[parseInt(f.slice(0,2),16),parseInt(f.slice(2,4),16),parseInt(f.slice(4,6),16)]:null}function _t(b){const d=ot(b);return d?(d[0]*299+d[1]*587+d[2]*114)/1e3>140?"#111":"#fff":"#000"}function Wt(b,d,f){const _=ot(b);if(!_)return b;const q=Math.round(d[0]+(_[0]-d[0])*f),W=Math.round(d[1]+(_[1]-d[1])*f),l=Math.round(d[2]+(_[2]-d[2])*f);return`rgb(${q},${W},${l})`}export function renderCorrelationPanel(b){const{title:d="Test correlation matrix",items:f,testDefs:_,onSaveImage:q,onSelectPair:W}=b,{card:l,body:A,controlsRow:J}=Et(d,q,b.ownerDocument);A.style.overflowX="auto",l.style.alignSelf="start";let Y=mt,F=null;const X=l.ownerDocument.createElement("label");X.textContent="Max tests:",Object.assign(X.style,{color:x.label,fontSize:H.body,display:"flex",alignItems:"center",gap:C.xs});const v=l.ownerDocument.createElement("input");v.type="number",v.min=String(ft),v.max=String(pt),v.value=String(Y),Object.assign(v.style,{width:"52px",fontSize:H.body,padding:`${C.xxs} ${C.xs}`,background:x.menuBg,color:x.value,border:`1px solid ${x.menuBorder}`,borderRadius:nt.control}),v.addEventListener("change",()=>{const e=Math.max(ft,Math.min(pt,parseInt(v.value,10)||mt));v.value=String(e),e!==Y&&(Y=e,Q())}),X.appendChild(v),J.appendChild(X),f.length>1&&J.appendChild(At(f,F,e=>{F=e,Q()},{ownerDocument:l.ownerDocument}));function ht(){return F!==null?f[F]?[f[F]]:[]:f}if(b.onSaveText){const e=l.ownerDocument.createElement("button");e.type="button",e.textContent="Correlation CSV",Object.assign(e.style,{...It("outlined"),color:x.text}),St(e),e.addEventListener("click",()=>{const s=st;if(!s)return;const u=["Test X,Test X number,Test Y,Test Y number,r,n"];for(let r=0;r<s.tests.length;r++)for(let p=r+1;p<s.tests.length;p++){const i=s.cells.find(w=>w.xIndex===p&&w.yIndex===r);i&&u.push([it(s.tests[p].label),String(s.tests[p].testNumber),it(s.tests[r].label),String(s.tests[r].testNumber),i.r===null?"":i.r.toFixed(6),String(i.n)].join(","))}Tt(u.join(`
2
+ `),"test-correlation.csv","text/csv",b.onSaveText)}),J.appendChild(e)}const B=l.ownerDocument.createElement("div");Object.assign(B.style,{display:"flex",flexDirection:"column",gap:C.xs,marginBottom:C.sm}),l.insertBefore(B,A);const L=Rt(l);$t(X,l,L,"Cap on how many tests the matrix includes (strongest correlations kept first)");let K=()=>{};function xt(e,s,u,r,p,i,w){B.innerHTML="";const g=vt(w);if(g){const m=l.ownerDocument.createElement("div");m.textContent=`${g} \u2014 r is an estimate, not the whole population.`,Object.assign(m.style,{color:x.label,fontSize:H.body}),B.appendChild(m)}if(p.length>0){const m=l.ownerDocument.createElement("div");m.textContent=`\u26A0 Mixed ${p.join(", ")} within this set \u2014 correlations may be misleading (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,Object.assign(m.style,{color:x.warnText,background:x.warnBg,border:`1px solid ${x.warnBorder}`,borderRadius:nt.control,padding:`${C.xs} ${C.md}`,fontSize:H.body}),B.appendChild(m)}const S=l.ownerDocument.createElement("span");Object.assign(S.style,{display:"inline-flex",alignItems:"center",gap:C.sm,color:x.label,fontSize:H.body,flexWrap:"wrap"});const I=l.ownerDocument.createElement("span");I.textContent=i!==null?`Pearson r \xB7 n \u2248 ${i.toLocaleString()} dies per pair \xB7 click a cell to view that pair in scatter \xB7`:"Pearson r \xB7 click a cell to view that pair in scatter \xB7",S.appendChild(I);const O=l.ownerDocument.createElement("span");Object.assign(O.style,{display:"inline-flex",alignItems:"center",gap:C.xs});const a=l.ownerDocument.createElement("span");a.textContent="\u22121";const T=l.ownerDocument.createElement("span");Object.assign(T.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:nt.control,border:`1px solid ${x.menuBorder}`,background:`linear-gradient(to right, ${ct}, ${x.menuBg}, ${at})`});const z=l.ownerDocument.createElement("span");z.textContent="+1",O.append(a,T,z),S.appendChild(O),B.appendChild(S);const $=l.ownerDocument.createElement("span");if(Object.assign($.style,{color:x.value,fontSize:H.body,fontWeight:"500"}),e===0&&s===0)$.textContent=r?`No significant correlations found \u2014 strongest pair: ${r.xLabel} \u2194 ${r.yLabel} (r = ${r.r.toFixed(2)})`:"No significant correlations found";else{const m=[];e>0&&m.push(`${e} strong (|r| \u2265 0.7)`),s>0&&m.push(`${s} moderate (0.4\u20130.7)`);const P=e+s;let o=m.join(", ")+` pair${P!==1?"s":""} found`;u>0&&(o+=` \xB7 ${u} weak pair${u!==1?"s":""} not shown`),$.textContent=o}B.appendChild($)}function bt(e){if(A.innerHTML="",e.tests.length<2)return Bt(A,"Need at least two parametric tests for a correlation matrix."),()=>{};const s=l.ownerDocument.createElement("canvas");s.style.display="block",s.style.cursor="default",A.appendChild(s);const u=e.tests.length,r=o=>o.label.split(" (#")[0],p=Math.min(14,Ct(e.tests.map(o=>r(o).length))),i=p*6.5+8,w=10,g=Math.round(w*6.5*Math.sin(Math.PI/4))+14,S=14,I=28,O=44;let a=-1,T=-1;function z(o){const c=Math.max(0,o-i);return Math.max(S,Math.min(O,Math.floor(c/u)))}const $=new Map;for(const o of e.cells){let c=$.get(o.yIndex);c||(c=[],$.set(o.yIndex,c)),c.push(o)}function m(){const o=Ot(l),c=ot(o.bg)??[255,255,255],h=A.clientWidth,n=z(h),j=n*u,E=g+n*u+4,D=i+j,R=Nt(s,l,D,E);if(!R)return;const{ctx:t}=R;t.font=`${dt(-1)}px system-ui, sans-serif`,t.fillStyle=o.text,e.tests.forEach((k,y)=>{const M=r(k),N=M.length>w?`${M.slice(0,w-1)}\u2026`:M,Z=i+y*n+n/2;t.save(),t.translate(Z,g-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=y===a?o.text:o.textMuted,t.fillText(N,0,0),t.restore()}),e.tests.forEach((k,y)=>{const M=r(k),N=g+y*n,Z=N+n/2;t.font=`${dt(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle",t.fillStyle=y===T?o.text:o.textMuted,t.fillText(M.length>p?`${M.slice(0,p-1)}\u2026`:M,i-4,Z),($.get(y)??[]).forEach(lt=>{const tt=lt.xIndex,V=i+tt*n,yt=tt===a&&y===T,rt=tt===y,G=lt.r;if(G===null){t.fillStyle=o.border,t.globalAlpha=.4,t.fillRect(V+1,N+1,n-2,n-2),t.globalAlpha=1;return}let et=o.bgHover;rt?t.fillStyle=o.bgHover:(et=Wt(G>=0?at:ct,c,Math.abs(G)),t.fillStyle=et),t.fillRect(V+1,N+1,n-2,n-2),n>=I&&!rt&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=_t(et),t.textAlign="center",t.textBaseline="middle",t.fillText(G.toFixed(2),V+n/2,N+n/2)),yt&&(t.strokeStyle=o.text,t.lineWidth=2,t.strokeRect(V+1,N+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=o.bg,t.lineWidth=1;for(let k=0;k<=u;k++){const y=i+k*n;t.beginPath(),t.moveTo(y,g),t.lineTo(y,g+u*n),t.stroke();const M=g+k*n;t.beginPath(),t.moveTo(i,M),t.lineTo(i+u*n,M),t.stroke()}}function P(o){const c=s.getBoundingClientRect(),h=A.clientWidth,n=z(h),j=(o.clientX-c.left)*(s.width/ut(s)/c.width),E=(o.clientY-c.top)*(s.height/ut(s)/c.height),D=Math.floor((j-i)/n),R=Math.floor((E-g)/n);return D>=0&&D<u&&R>=0&&R<u?{xi:D,yi:R}:null}return s.addEventListener("mousemove",o=>{const c=P(o);if(!c){L.style.display="none",s.style.cursor="default";return}const{xi:h,yi:n}=c,j=h===n;s.style.cursor=j||!W?"default":"pointer";const E=e.cells.find(t=>t.xIndex===h&&t.yIndex===n),D=e.tests[h].label,R=e.tests[n].label;j?L.innerHTML=`<strong>${U(D)}</strong>`:E?.r!==null&&E?.r!==void 0?L.innerHTML=`<strong>${U(r(e.tests[n]))}</strong> (#${e.tests[n].testNumber}) vs <strong>${U(r(e.tests[h]))}</strong> (#${e.tests[h].testNumber})<br>r = ${E.r.toFixed(4)} \xB7 n = ${E.n.toLocaleString()}${W?"<br><em>click to view in scatter</em>":""}`:L.innerHTML=`${U(`${R} vs ${D}`)}<br><em>insufficient data</em>`,L.style.display="block",kt(L,l,o.clientX,o.clientY)}),s.addEventListener("mouseleave",()=>{L.style.display="none"}),s.addEventListener("click",o=>{const c=P(o);if(!c||!W)return;const{xi:h,yi:n}=c;h!==n&&(a=h,T=n,m(),W(e.tests[h].testNumber,e.tests[n].testNumber))}),m}let st=null;function Q(){const e=ht(),s=e.flatMap(a=>a.dies??[]),u=wt(s,_),{matrix:r,strongPairs:p,moderatePairs:i,hiddenWeakPairs:w,strongestPair:g}=Mt(u,{minTests:6,maxTests:Y}),S=Lt(e,{facetableOnly:!0}).filter(a=>a.splittable).map(a=>a.label),I=r.cells.filter(a=>a.xIndex!==a.yIndex).map(a=>a.n).sort((a,T)=>a-T),O=I.length?I[Math.floor(I.length/2)]:null;st=r,xt(p,i,w,g,S,O,u.sample),K=bt(r),K()}const gt=Dt(l,()=>K());return Q(),{card:l,destroy:()=>gt.disconnect()}}
@@ -1 +1 @@
1
- import{buildTestHistogramData as _t,collectTestValues as At,buildTestHistogramSeries as Ut,testValueExtent as Ct}from"../../stats/histogram.js";import{SPACE as at,fontPx as Q,FONT as $t,CLR as Pt}from"../toolbar.js";import{fmt as bt}from"../../renderer/fmt.js";import{QUANTITY as Xt,categorical as Yt}from"./palette.js";import{cardShell as Qt,observeResize as Wt,makeTooltip as qt,attachChartTip as Jt,positionChartTooltip as Dt,makeLinkedTestSelect as Zt,makeWaferSelect as te,makeLinkedAxisPrefs as ee,renderEmptyState as Tt,chartFillHeight as Et,applyCanvasFlow as It,makeAxisFormat as Bt,PADDING as ft,robustFence as Ft,shouldIncludeLimitsByDefault as Kt,drawOffAxisLimits as ne,resolveAxisRange as ie,chartSwatchCss as oe,makeSeriesLegendItem as le,prepareCanvas as Ot}from"./chartShell.js";import{escHtml as St}from"../../core/utils.js";const wt=230,Rt=36,yt=18;function Nt(l,G,q,J,Mt,B,r,y=4){const Z=Math.max(1,B/y),rt=Math.pow(10,Math.floor(Math.log10(Z))),d=Z/rt,tt=(d<=1?1:d<=2?2:d<=5?5:10)*rt,K=J-q;l.save(),l.font=`${Q(-1)}px system-ui, sans-serif`,l.textAlign="right",l.textBaseline="middle";for(let V=0;V<=B+1e-9;V+=tt){const ct=J-V/(B||1)*K;l.strokeStyle=r.grid,l.lineWidth=1,l.beginPath(),l.moveTo(G,ct),l.lineTo(G+Mt,ct),l.stroke(),l.fillStyle=r.text,l.fillText(String(Math.round(V)),G-4,ct)}l.strokeStyle=r.axis,l.lineWidth=1,l.beginPath(),l.moveTo(G,q),l.lineTo(G,J),l.stroke(),l.restore()}export function renderHistogramPanel(l){const{title:G="Test value distribution",items:q,testDefs:J,onSaveImage:Mt,groups:B}=l,{card:r,body:y,controlsRow:Z}=Qt(G,Mt,l.ownerDocument),rt=J.filter(s=>s.testNumber!==void 0);let d=l.selectedTestNumber??rt[0]?.testNumber??null,tt=null,K=0;const V=Zt(rt,d,s=>{d=s,j(),l.onTestChange?.(s)},{maxWidth:"200px",ownerDocument:r.ownerDocument}),ct=V.el;Z.appendChild(ct);const vt=te(q,tt,s=>{tt=s,j()},{ownerDocument:r.ownerDocument});Z.appendChild(vt);function zt(s){return S===s||s!==null&&!(B??[]).some(v=>v.key===s)?!1:(S=s,j(),!0)}const Lt=r.ownerDocument.createElement("span");Object.assign(Lt.style,{display:"inline-flex",gap:at.lg,alignItems:"center"}),Z.appendChild(Lt);const gt=ee(Lt,l.axisPrefs,s=>{l.onAxisPrefsChange?.(s),j()},r.ownerDocument),C=qt(r);let dt=null,S=null;function et(s){const v=J.find(nt=>nt.testNumber===s);return{unit:v?.unit,limitLow:v?.limitLow,limitHigh:v?.limitHigh}}function j(){const{includeLimits:s,clipOutliers:v}=gt.get();if(dt?.disconnect(),dt=null,y.innerHTML="",d===null){Tt(y,"No parametric test data available for a histogram.");return}const nt=!!B&&B.length>0;let it=!1,u;if(nt){const{limitLow:a,limitHigh:m}=et(d),o=B.flatMap(x=>x.items),{min:e,max:p}=Ct(o,d);if(it=s??Kt(e,p,a,m),v){const x=At(o,d),H=Ft(x);H&&(u={lo:Math.max(e,H.lo),hi:Math.min(p,H.hi)},K=x.reduce((lt,E)=>lt+(E<u.lo||E>u.hi?1:0),0))}u||(K=0)}const $=nt?Ut(B,d,16,it?et(d).limitLow:void 0,it?et(d).limitHigh:void 0,u):null;if(vt.style.display=$?"none":"",$){const{limitLow:a,limitHigh:m}=et(d);if(gt.sync(it,a!==void 0||m!==void 0),$.series.length===0){Tt(y,"No parametric test data available for a histogram.");return}Gt($);return}const _=tt!==null?[q[tt]]:q,{unit:O,limitLow:P,limitHigh:W}=et(d),k=At(_,d),{min:ot,max:mt}=Ct(_,d),ut=s??Kt(ot,mt,P,W);gt.sync(ut,P!==void 0||W!==void 0);const U=v?Ft(k):null,X=U?{lo:Math.max(ot,U.lo),hi:Math.min(mt,U.hi)}:void 0;K=X?k.filter(a=>a<X.lo||a>X.hi).length:0;const h=_t(_,d,16,ut?P:void 0,ut?W:void 0,X);if(h.length===0){Tt(y,"No parametric test data available for a histogram.");return}const D=Math.max(...h.map(a=>a.count),1),Y=r.ownerDocument.createElement("div");Object.assign(Y.style,{fontSize:$t.body,color:Pt.label,marginBottom:at.xxs}),Y.textContent=`max ${D} dies/bucket`+(K?` \xB7 ${K} value${K===1?"":"s"} outside clipped range`:""),y.appendChild(Y);const M=r.ownerDocument.createElement("canvas");M.style.display="block",M.style.cursor="default",y.appendChild(M);let F=-1;const n=h[0].rangeLow,g=h[h.length-1].rangeHigh,L=g-n||1,t=Bt(Math.max(Math.abs(n),Math.abs(g)),O);function c(a){const m=ft+36,o=M.clientWidth-m-ft,e=a-Rt-yt;return{plotX:m,plotMaxWidth:Math.max(10,o),plotMaxHeight:e,plotTop:yt}}function f(a){const{plotX:m,plotMaxWidth:o}=c(wt);if(a<m||a>m+o)return-1;const e=Math.floor((a-m)/o*h.length);return e>=0&&e<h.length?e:-1}function T(){It(M,Y);const a=y.clientWidth,m=Et(r,y,M,wt),o=Ot(M,r,a,m);if(!o)return;const{ctx:e,theme:p}=o,{plotX:x,plotMaxWidth:H,plotMaxHeight:lt,plotTop:E}=c(m),i=E+lt,w=H/h.length;Nt(e,x,E,i,H,D,{text:p.textMuted,axis:p.border,grid:p.border}),h.forEach((b,z)=>{const I=b.count/D*lt,kt=x+z*w,Ht=i-I;e.fillStyle=z===F?p.text:Xt,e.globalAlpha=z===F?1:.7,e.fillRect(kt+1,Ht,Math.max(1,w-2),I),e.globalAlpha=1,z===F&&(e.fillStyle=p.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${b.count}`,kt+w/2,Ht-2))});const A=b=>x+(b-n)/L*H;e.font=`${Q(-1)}px system-ui, sans-serif`;const{offAxis:ht}=ie({dataMin:n,dataMax:g,limitLow:P,limitHigh:W,includeLimits:!1});ne(e,ht,{left:x,right:x+H,top:E,bottom:i},"horizontal",p.limitLine,t.tick);const pt=new Set(ht.map(b=>b.value));for(const[b,z]of[[P,"LSL"],[W,"USL"]]){if(b===void 0||pt.has(b))continue;const I=A(b);e.strokeStyle=p.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(I,E),e.lineTo(I,i),e.stroke(),e.setLineDash([]),e.fillStyle=p.limitLine,e.textAlign="center",e.textBaseline="bottom",e.fillText(z,I,E-2)}e.font=`${Q(-1)}px system-ui, sans-serif`,e.strokeStyle=p.border,e.lineWidth=1,e.beginPath(),e.moveTo(x,i),e.lineTo(x+H,i),e.stroke(),e.fillStyle=p.textMuted,e.textAlign="center",e.textBaseline="top";const st=Math.max(2,Math.floor(H/44)),R=Math.ceil(h.length/st),N=Math.max(1,R);for(let b=0;b<=h.length;b+=N){const z=b<h.length?h[b].rangeLow:h[h.length-1].rangeHigh,I=x+b*w;e.beginPath(),e.moveTo(I,i),e.lineTo(I,i+4),e.stroke(),e.fillText(t.tick(z),I,i+6)}t.unitLabel&&(e.save(),e.font=`${Q(-1)}px system-ui, sans-serif`,e.fillStyle=p.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${t.unitLabel})`,x+H,i+20),e.restore())}M.addEventListener("mousemove",a=>{const m=M.getBoundingClientRect(),o=f(a.clientX-m.left);if(o!==F&&(F=o,T()),o>=0){const e=h[o];C.innerHTML=`<strong>${St(`${bt(e.rangeLow,O,"engineering")} \u2013 ${bt(e.rangeHigh,O,"engineering")}`)}</strong><br>${e.count} dies`,C.style.display="block",Dt(C,r,a.clientX,a.clientY)}else C.style.display="none"}),M.addEventListener("mouseleave",()=>{F!==-1&&(F=-1,T()),C.style.display="none"}),dt=Wt(r,()=>T()),T()}function Gt(s){const{unit:v,limitLow:nt,limitHigh:it}=et(d),u=s.ranges,$=s.series,_=Yt;S&&!$.some(n=>n.groupKey===S)&&(S=null);const O=Math.max(1,...$.flatMap(n=>n.counts)),P=r.ownerDocument.createElement("div");Object.assign(P.style,{fontSize:$t.body,color:Pt.label,marginBottom:at.xxs}),P.textContent=`${$.length} groups \xB7 max ${O} dies/bucket`,y.appendChild(P);const W=r.ownerDocument.createElement("div");Object.assign(W.style,{display:"flex",flexWrap:"wrap",gap:at.sm,marginTop:at.md,marginBottom:at.xl}),$.forEach((n,g)=>{const L=le(n.groupKey,_(g),r.ownerDocument),t=L.el;Jt(t,r,C,`${n.groupKey} \u2014 click to emphasize (dim the rest)`),L.setState({selected:S===n.groupKey,dimmed:S!==null&&S!==n.groupKey}),t.addEventListener("click",()=>{const c=S===n.groupKey?null:n.groupKey;zt(c),l.onGroupChange?.(c)}),W.appendChild(t)}),y.appendChild(W);const k=r.ownerDocument.createElement("canvas");k.style.display="block",k.style.cursor="default",y.appendChild(k);const ot=u[0].rangeLow,mt=u[u.length-1].rangeHigh,ut=mt-ot||1,U=Bt(Math.max(Math.abs(ot),Math.abs(mt)),v),X=n=>{const g=(n.ownerDocument.defaultView??window).getComputedStyle(n);return n.offsetHeight+(parseFloat(g.marginTop)||0)+(parseFloat(g.marginBottom)||0)},h=()=>X(P)+X(W);let D=-1,Y={plotX:ft+36,plotMaxWidth:10,barWidth:10};function M(){It(k,h());const n=y.clientWidth,g=Et(r,y,k,wt),L=Ot(k,r,n,g);if(!L)return;const{ctx:t,theme:c}=L,f=ft+36,T=Math.max(10,n-f-ft),a=yt,m=g-Rt-yt,o=a+m,e=T/u.length;Y={plotX:f,plotMaxWidth:T,barWidth:e},Nt(t,f,a,o,T,O,{text:c.textMuted,axis:c.border,grid:c.border}),D>=0&&(t.fillStyle=c.bgHover,t.globalAlpha=.5,t.fillRect(f+D*e,a,e,m),t.globalAlpha=1);const p=$.map((i,w)=>({s:i,i:w})).sort((i,w)=>(i.s.groupKey===S?1:0)-(w.s.groupKey===S?1:0));for(const{s:i,i:w}of p){const A=S===i.groupKey,ht=S!==null&&!A,pt=_(w);t.strokeStyle=pt,t.fillStyle=pt,t.lineWidth=A?2:1,t.globalAlpha=ht?.12:S?.32:.22,t.beginPath(),t.moveTo(f,o),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/O*m;t.lineTo(R,N),t.lineTo(R+e,N)}),t.lineTo(f+T,o),t.closePath(),t.fill(),t.globalAlpha=ht?.3:1,t.beginPath(),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/O*m;st===0?t.moveTo(R,N):t.lineTo(R,N),t.lineTo(R+e,N)}),t.stroke()}t.globalAlpha=1;const x=i=>f+(i-ot)/ut*T;t.font=`${Q(-1)}px system-ui, sans-serif`;for(const[i,w]of[[nt,"LSL"],[it,"USL"]]){if(i===void 0)continue;const A=x(i);t.strokeStyle=c.limitLine,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(A,a),t.lineTo(A,o),t.stroke(),t.setLineDash([]),t.fillStyle=c.limitLine,t.textAlign="center",t.textBaseline="bottom",t.fillText(w,A,a-2)}t.font=`${Q(-1)}px system-ui, sans-serif`,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.moveTo(f,o),t.lineTo(f+T,o),t.stroke(),t.fillStyle=c.textMuted,t.textAlign="center",t.textBaseline="top";const lt=Math.max(2,Math.floor(T/44)),E=Math.max(1,Math.ceil(u.length/lt));for(let i=0;i<=u.length;i+=E){const w=i<u.length?u[i].rangeLow:u[u.length-1].rangeHigh,A=f+i*e;t.beginPath(),t.moveTo(A,o),t.lineTo(A,o+4),t.stroke(),t.fillText(U.tick(w),A,o+6)}U.unitLabel&&(t.save(),t.font=`${Q(-1)}px system-ui, sans-serif`,t.fillStyle=c.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${U.unitLabel})`,f+T,o+20),t.restore())}function F(n){const{plotX:g,plotMaxWidth:L,barWidth:t}=Y;if(n<g||n>g+L)return-1;const c=Math.floor((n-g)/t);return c>=0&&c<u.length?c:-1}k.addEventListener("mousemove",n=>{const g=k.getBoundingClientRect(),L=F(n.clientX-g.left);if(L!==D&&(D=L,M()),L>=0){const t=u[L],c=$.map((f,T)=>`<span style="${oe(_(T))};margin-right:4px"></span>${St(`${f.groupKey}: ${f.counts[L]}`)}`).join("<br>");C.innerHTML=`<strong>${St(`${bt(t.rangeLow,v,"engineering")} \u2013 ${bt(t.rangeHigh,v,"engineering")}`)}</strong><br>${c}`,C.style.display="block",Dt(C,r,n.clientX,n.clientY)}else C.style.display="none"}),k.addEventListener("mouseleave",()=>{D!==-1&&(D=-1,M()),C.style.display="none"}),dt=Wt(r,()=>M()),M()}j();function Vt(s){V.set(s)&&(d=s,j())}function jt(s){gt.set(s)&&j()}return{card:r,setTest:Vt,setAxisPrefs:jt,destroy:()=>dt?.disconnect()}}
1
+ import{buildTestHistogramData as _t,collectTestValues as At,buildTestHistogramSeries as Ut,testValueExtent as Ct}from"../../stats/histogram.js";import{SPACE as at,fontPx as Q,FONT as $t,CLR as Pt}from"../toolbar.js";import{fmt as bt}from"../../renderer/fmt.js";import{QUANTITY as Xt,categorical as Yt}from"./palette.js";import{cardShell as Qt,observeResize as Wt,makeTooltip as qt,attachChartTip as Jt,positionChartTooltip as Dt,makeLinkedTestSelect as Zt,makeWaferSelect as te,makeLinkedAxisPrefs as ee,renderEmptyState as Tt,chartFillHeight as Et,applyCanvasFlow as It,makeAxisFormat as Bt,PADDING as ft,robustFence as Ot,shouldIncludeLimitsByDefault as Ft,drawOffAxisLimits as ne,resolveAxisRange as ie,chartSwatchCss as oe,makeSeriesLegendItem as le,prepareCanvas as Kt}from"./chartShell.js";import{escHtml as St,maxOf as se}from"../../core/utils.js";const wt=230,Rt=36,yt=18;function Nt(l,G,q,J,Mt,B,r,y=4){const Z=Math.max(1,B/y),rt=Math.pow(10,Math.floor(Math.log10(Z))),d=Z/rt,tt=(d<=1?1:d<=2?2:d<=5?5:10)*rt,F=J-q;l.save(),l.font=`${Q(-1)}px system-ui, sans-serif`,l.textAlign="right",l.textBaseline="middle";for(let V=0;V<=B+1e-9;V+=tt){const ct=J-V/(B||1)*F;l.strokeStyle=r.grid,l.lineWidth=1,l.beginPath(),l.moveTo(G,ct),l.lineTo(G+Mt,ct),l.stroke(),l.fillStyle=r.text,l.fillText(String(Math.round(V)),G-4,ct)}l.strokeStyle=r.axis,l.lineWidth=1,l.beginPath(),l.moveTo(G,q),l.lineTo(G,J),l.stroke(),l.restore()}export function renderHistogramPanel(l){const{title:G="Test value distribution",items:q,testDefs:J,onSaveImage:Mt,groups:B}=l,{card:r,body:y,controlsRow:Z}=Qt(G,Mt,l.ownerDocument),rt=J.filter(s=>s.testNumber!==void 0);let d=l.selectedTestNumber??rt[0]?.testNumber??null,tt=null,F=0;const V=Zt(rt,d,s=>{d=s,j(),l.onTestChange?.(s)},{maxWidth:"200px",ownerDocument:r.ownerDocument}),ct=V.el;Z.appendChild(ct);const vt=te(q,tt,s=>{tt=s,j()},{ownerDocument:r.ownerDocument});Z.appendChild(vt);function zt(s){return S===s||s!==null&&!(B??[]).some(v=>v.key===s)?!1:(S=s,j(),!0)}const Lt=r.ownerDocument.createElement("span");Object.assign(Lt.style,{display:"inline-flex",gap:at.lg,alignItems:"center"}),Z.appendChild(Lt);const gt=ee(Lt,l.axisPrefs,s=>{l.onAxisPrefsChange?.(s),j()},r.ownerDocument),C=qt(r);let dt=null,S=null;function et(s){const v=J.find(nt=>nt.testNumber===s);return{unit:v?.unit,limitLow:v?.limitLow,limitHigh:v?.limitHigh}}function j(){const{includeLimits:s,clipOutliers:v}=gt.get();if(dt?.disconnect(),dt=null,y.innerHTML="",d===null){Tt(y,"No parametric test data available for a histogram.");return}const nt=!!B&&B.length>0;let it=!1,u;if(nt){const{limitLow:a,limitHigh:m}=et(d),o=B.flatMap(x=>x.items),{min:e,max:p}=Ct(o,d);if(it=s??Ft(e,p,a,m),v){const x=At(o,d),H=Ot(x);H&&(u={lo:Math.max(e,H.lo),hi:Math.min(p,H.hi)},F=x.reduce((lt,E)=>lt+(E<u.lo||E>u.hi?1:0),0))}u||(F=0)}const $=nt?Ut(B,d,16,it?et(d).limitLow:void 0,it?et(d).limitHigh:void 0,u):null;if(vt.style.display=$?"none":"",$){const{limitLow:a,limitHigh:m}=et(d);if(gt.sync(it,a!==void 0||m!==void 0),$.series.length===0){Tt(y,"No parametric test data available for a histogram.");return}Gt($);return}const _=tt!==null?[q[tt]]:q,{unit:K,limitLow:P,limitHigh:W}=et(d),k=At(_,d),{min:ot,max:mt}=Ct(_,d),ut=s??Ft(ot,mt,P,W);gt.sync(ut,P!==void 0||W!==void 0);const U=v?Ot(k):null,X=U?{lo:Math.max(ot,U.lo),hi:Math.min(mt,U.hi)}:void 0;F=X?k.filter(a=>a<X.lo||a>X.hi).length:0;const h=_t(_,d,16,ut?P:void 0,ut?W:void 0,X);if(h.length===0){Tt(y,"No parametric test data available for a histogram.");return}const D=Math.max(se(h.map(a=>a.count)),1),Y=r.ownerDocument.createElement("div");Object.assign(Y.style,{fontSize:$t.body,color:Pt.label,marginBottom:at.xxs}),Y.textContent=`max ${D} dies/bucket`+(F?` \xB7 ${F} value${F===1?"":"s"} outside clipped range`:""),y.appendChild(Y);const M=r.ownerDocument.createElement("canvas");M.style.display="block",M.style.cursor="default",y.appendChild(M);let O=-1;const n=h[0].rangeLow,g=h[h.length-1].rangeHigh,L=g-n||1,t=Bt(Math.max(Math.abs(n),Math.abs(g)),K);function c(a){const m=ft+36,o=M.clientWidth-m-ft,e=a-Rt-yt;return{plotX:m,plotMaxWidth:Math.max(10,o),plotMaxHeight:e,plotTop:yt}}function f(a){const{plotX:m,plotMaxWidth:o}=c(wt);if(a<m||a>m+o)return-1;const e=Math.floor((a-m)/o*h.length);return e>=0&&e<h.length?e:-1}function T(){It(M,Y);const a=y.clientWidth,m=Et(r,y,M,wt),o=Kt(M,r,a,m);if(!o)return;const{ctx:e,theme:p}=o,{plotX:x,plotMaxWidth:H,plotMaxHeight:lt,plotTop:E}=c(m),i=E+lt,w=H/h.length;Nt(e,x,E,i,H,D,{text:p.textMuted,axis:p.border,grid:p.border}),h.forEach((b,z)=>{const I=b.count/D*lt,kt=x+z*w,Ht=i-I;e.fillStyle=z===O?p.text:Xt,e.globalAlpha=z===O?1:.7,e.fillRect(kt+1,Ht,Math.max(1,w-2),I),e.globalAlpha=1,z===O&&(e.fillStyle=p.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${b.count}`,kt+w/2,Ht-2))});const A=b=>x+(b-n)/L*H;e.font=`${Q(-1)}px system-ui, sans-serif`;const{offAxis:ht}=ie({dataMin:n,dataMax:g,limitLow:P,limitHigh:W,includeLimits:!1});ne(e,ht,{left:x,right:x+H,top:E,bottom:i},"horizontal",p.limitLine,t.tick);const pt=new Set(ht.map(b=>b.value));for(const[b,z]of[[P,"LSL"],[W,"USL"]]){if(b===void 0||pt.has(b))continue;const I=A(b);e.strokeStyle=p.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(I,E),e.lineTo(I,i),e.stroke(),e.setLineDash([]),e.fillStyle=p.limitLine,e.textAlign="center",e.textBaseline="bottom",e.fillText(z,I,E-2)}e.font=`${Q(-1)}px system-ui, sans-serif`,e.strokeStyle=p.border,e.lineWidth=1,e.beginPath(),e.moveTo(x,i),e.lineTo(x+H,i),e.stroke(),e.fillStyle=p.textMuted,e.textAlign="center",e.textBaseline="top";const st=Math.max(2,Math.floor(H/44)),R=Math.ceil(h.length/st),N=Math.max(1,R);for(let b=0;b<=h.length;b+=N){const z=b<h.length?h[b].rangeLow:h[h.length-1].rangeHigh,I=x+b*w;e.beginPath(),e.moveTo(I,i),e.lineTo(I,i+4),e.stroke(),e.fillText(t.tick(z),I,i+6)}t.unitLabel&&(e.save(),e.font=`${Q(-1)}px system-ui, sans-serif`,e.fillStyle=p.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${t.unitLabel})`,x+H,i+20),e.restore())}M.addEventListener("mousemove",a=>{const m=M.getBoundingClientRect(),o=f(a.clientX-m.left);if(o!==O&&(O=o,T()),o>=0){const e=h[o];C.innerHTML=`<strong>${St(`${bt(e.rangeLow,K,"engineering")} \u2013 ${bt(e.rangeHigh,K,"engineering")}`)}</strong><br>${e.count} dies`,C.style.display="block",Dt(C,r,a.clientX,a.clientY)}else C.style.display="none"}),M.addEventListener("mouseleave",()=>{O!==-1&&(O=-1,T()),C.style.display="none"}),dt=Wt(r,()=>T()),T()}function Gt(s){const{unit:v,limitLow:nt,limitHigh:it}=et(d),u=s.ranges,$=s.series,_=Yt;S&&!$.some(n=>n.groupKey===S)&&(S=null);const K=Math.max(1,...$.flatMap(n=>n.counts)),P=r.ownerDocument.createElement("div");Object.assign(P.style,{fontSize:$t.body,color:Pt.label,marginBottom:at.xxs}),P.textContent=`${$.length} groups \xB7 max ${K} dies/bucket`,y.appendChild(P);const W=r.ownerDocument.createElement("div");Object.assign(W.style,{display:"flex",flexWrap:"wrap",gap:at.sm,marginTop:at.md,marginBottom:at.xl}),$.forEach((n,g)=>{const L=le(n.groupKey,_(g),r.ownerDocument),t=L.el;Jt(t,r,C,`${n.groupKey} \u2014 click to emphasize (dim the rest)`),L.setState({selected:S===n.groupKey,dimmed:S!==null&&S!==n.groupKey}),t.addEventListener("click",()=>{const c=S===n.groupKey?null:n.groupKey;zt(c),l.onGroupChange?.(c)}),W.appendChild(t)}),y.appendChild(W);const k=r.ownerDocument.createElement("canvas");k.style.display="block",k.style.cursor="default",y.appendChild(k);const ot=u[0].rangeLow,mt=u[u.length-1].rangeHigh,ut=mt-ot||1,U=Bt(Math.max(Math.abs(ot),Math.abs(mt)),v),X=n=>{const g=(n.ownerDocument.defaultView??window).getComputedStyle(n);return n.offsetHeight+(parseFloat(g.marginTop)||0)+(parseFloat(g.marginBottom)||0)},h=()=>X(P)+X(W);let D=-1,Y={plotX:ft+36,plotMaxWidth:10,barWidth:10};function M(){It(k,h());const n=y.clientWidth,g=Et(r,y,k,wt),L=Kt(k,r,n,g);if(!L)return;const{ctx:t,theme:c}=L,f=ft+36,T=Math.max(10,n-f-ft),a=yt,m=g-Rt-yt,o=a+m,e=T/u.length;Y={plotX:f,plotMaxWidth:T,barWidth:e},Nt(t,f,a,o,T,K,{text:c.textMuted,axis:c.border,grid:c.border}),D>=0&&(t.fillStyle=c.bgHover,t.globalAlpha=.5,t.fillRect(f+D*e,a,e,m),t.globalAlpha=1);const p=$.map((i,w)=>({s:i,i:w})).sort((i,w)=>(i.s.groupKey===S?1:0)-(w.s.groupKey===S?1:0));for(const{s:i,i:w}of p){const A=S===i.groupKey,ht=S!==null&&!A,pt=_(w);t.strokeStyle=pt,t.fillStyle=pt,t.lineWidth=A?2:1,t.globalAlpha=ht?.12:S?.32:.22,t.beginPath(),t.moveTo(f,o),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/K*m;t.lineTo(R,N),t.lineTo(R+e,N)}),t.lineTo(f+T,o),t.closePath(),t.fill(),t.globalAlpha=ht?.3:1,t.beginPath(),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/K*m;st===0?t.moveTo(R,N):t.lineTo(R,N),t.lineTo(R+e,N)}),t.stroke()}t.globalAlpha=1;const x=i=>f+(i-ot)/ut*T;t.font=`${Q(-1)}px system-ui, sans-serif`;for(const[i,w]of[[nt,"LSL"],[it,"USL"]]){if(i===void 0)continue;const A=x(i);t.strokeStyle=c.limitLine,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(A,a),t.lineTo(A,o),t.stroke(),t.setLineDash([]),t.fillStyle=c.limitLine,t.textAlign="center",t.textBaseline="bottom",t.fillText(w,A,a-2)}t.font=`${Q(-1)}px system-ui, sans-serif`,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.moveTo(f,o),t.lineTo(f+T,o),t.stroke(),t.fillStyle=c.textMuted,t.textAlign="center",t.textBaseline="top";const lt=Math.max(2,Math.floor(T/44)),E=Math.max(1,Math.ceil(u.length/lt));for(let i=0;i<=u.length;i+=E){const w=i<u.length?u[i].rangeLow:u[u.length-1].rangeHigh,A=f+i*e;t.beginPath(),t.moveTo(A,o),t.lineTo(A,o+4),t.stroke(),t.fillText(U.tick(w),A,o+6)}U.unitLabel&&(t.save(),t.font=`${Q(-1)}px system-ui, sans-serif`,t.fillStyle=c.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${U.unitLabel})`,f+T,o+20),t.restore())}function O(n){const{plotX:g,plotMaxWidth:L,barWidth:t}=Y;if(n<g||n>g+L)return-1;const c=Math.floor((n-g)/t);return c>=0&&c<u.length?c:-1}k.addEventListener("mousemove",n=>{const g=k.getBoundingClientRect(),L=O(n.clientX-g.left);if(L!==D&&(D=L,M()),L>=0){const t=u[L],c=$.map((f,T)=>`<span style="${oe(_(T))};margin-right:4px"></span>${St(`${f.groupKey}: ${f.counts[L]}`)}`).join("<br>");C.innerHTML=`<strong>${St(`${bt(t.rangeLow,v,"engineering")} \u2013 ${bt(t.rangeHigh,v,"engineering")}`)}</strong><br>${c}`,C.style.display="block",Dt(C,r,n.clientX,n.clientY)}else C.style.display="none"}),k.addEventListener("mouseleave",()=>{D!==-1&&(D=-1,M()),C.style.display="none"}),dt=Wt(r,()=>M()),M()}j();function Vt(s){V.set(s)&&(d=s,j())}function jt(s){gt.set(s)&&j()}return{card:r,setTest:Vt,setAxisPrefs:jt,destroy:()=>dt?.disconnect()}}
@@ -1 +1 @@
1
- import{NO_DATA_FILL as ae}from"../../renderer/colorMap.js";import{categorical as Te}from"./palette.js";import{buildScatterData as we,buildScatterDataGrouped as Me}from"../../stats/scatter.js";import{pearsonOfPairs as ve}from"../../stats/correlation.js";import{buildFacetTable as Le}from"../../stats/facets.js";import{SPACE as T,RADIUS as ke,fontPx as j,FONT as q,CLR as H}from"../toolbar.js";import{cardShell as Ce,observeResize as Ae,makeTooltip as De,attachChartTip as Oe,makeTestSelect as $e,makeWaferSelect as Be,chartFillHeight as Pe,applyCanvasFlow as He,drawAxisUnit as We,resolveChartCanvasColors as Ee,makeAxisFormat as re,makeSeriesLegendItem as Fe,prepareCanvas as Ie}from"./chartShell.js";const c=52,Ne=16,s=16,Re=44;export function renderScatterPanel(W){const{title:ce="Test scatter",items:C,testDefs:J,binColors:de,onSaveImage:me,groups:E}=W,{card:i,body:L,controlsRow:Q}=Ce(ce,me,W.ownerDocument),F=J.filter(t=>t.testNumber!==void 0),y=!!(E&&E.length>0),V=(E??[]).map(t=>t.key),fe=new Map(V.map((t,n)=>[t,n]));let p=W.xTestNumber??F[0]?.testNumber??null,x=W.yTestNumber??F[1]?.testNumber??p,A=null;const Z=()=>A!==null?C[A]?[C[A]]:[]:C;function ee(t,n,o){const l=i.ownerDocument.createElement("label");Object.assign(l.style,{display:"inline-flex",alignItems:"center",gap:T.xs,fontSize:q.body,color:H.label});const g=i.ownerDocument.createElement("span");g.textContent=t;const m=$e(F,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:i.ownerDocument});return l.append(g,m),{wrap:l,select:m}}const{wrap:pe,select:ue}=ee("X:",p,t=>{p=t,$()}),{wrap:xe,select:he}=ee("Y:",x,t=>{x=t,$()});Q.append(pe,xe),!y&&C.length>1&&Q.appendChild(Be(C,A,t=>{A=t,$()},{ownerDocument:i.ownerDocument}));const D=i.ownerDocument.createElement("div");Object.assign(D.style,{color:H.warnText,background:H.warnBg,border:`1px solid ${H.warnBorder}`,borderRadius:ke.control,padding:`${T.xs} ${T.md}`,fontSize:q.body,marginBottom:T.xs,display:"none"}),i.insertBefore(D,L);function te(){const t=y?[]:Le(Z(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);t.length>0?(D.textContent=`\u26A0 Mixed ${t.join(", ")} across these points \u2014 trends here may be confounded (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,D.style.display=""):D.style.display="none"}te();const U=i.ownerDocument.createElement("div");Object.assign(U.style,{color:H.label,fontSize:q.body,marginBottom:T.xs}),i.insertBefore(U,L);function G(){const t=y?"One point per die \xB7 coloured by group \xB7 click legend to filter":"One point per die across all wafers \xB7 coloured by hard bin \xB7 click legend to filter",n=h.size===0?f:f.filter(g=>h.has(I(g))),{r:o,n:l}=ve(n);U.textContent=o===null?`${t} \xB7 n = ${l.toLocaleString()}`:`${t} \xB7 r = ${o.toFixed(3)} \xB7 n = ${l.toLocaleString()}`}const I=t=>y?t.group??"\u2014":String(t.hbin??0),ne=t=>y?Te(fe.get(t)??0):t==="0"?ae:de?.get(Number(t))??ae,oe=t=>y?t:t==="0"?"No bin data":`Bin ${t}`,h=new Set,O=i.ownerDocument.createElement("div");Object.assign(O.style,{display:"flex",flexWrap:"wrap",gap:T.sm,marginTop:T.md,marginBottom:T.xl}),L.appendChild(O);const k=i.ownerDocument.createElement("canvas");k.style.display="block",k.style.cursor="crosshair",L.appendChild(k);const ge=De(i);let f=[],w=0,N=1,M=0,R=1;function z(t){const n=J.find(o=>o.testNumber===t);return{unit:n?.unit,limitLow:n?.limitLow,limitHigh:n?.limitHigh}}function be(){const t=L.clientWidth,n=Math.max(200,Math.min(400,t*.65)),o=Pe(i,L,k,n);return{w:t,h:o,plotW:Math.max(10,t-c-Ne),plotH:Math.max(10,o-s-Re)}}const X=new Map;function ie(){for(const[t,n]of X){const o=h.has(t);n.setState({selected:o,dimmed:h.size>0&&!o})}}function le(t){O.innerHTML="",X.clear(),h.clear();for(const n of t){const o=Fe(oe(n),ne(n),i.ownerDocument),l=o.el;l.dataset.cat=n,X.set(n,o),Oe(l,i,ge,`${oe(n)} \u2014 click to filter`),l.addEventListener("click",()=>{h.has(n)?h.delete(n):h.add(n),ie(),G(),_()}),O.appendChild(l)}ie()}function _(){He(k,O);const t=Ee(i),n=N-w,o=R-M,{w:l,h:g,plotW:m,plotH:d}=be(),B=Ie(k,i,l,g);if(!B)return;const{ctx:e}=B;if(f.length===0){e.font=`${j()}px system-ui, sans-serif`,e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.fillText("No data \u2014 select two parametric tests with values.",l/2,g/2);return}const b=4,Y=re(Math.max(Math.abs(w),Math.abs(N)),p!==null?z(p).unit:void 0),K=re(Math.max(Math.abs(M),Math.abs(R)),x!==null?z(x).unit:void 0);e.font=`${j(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=.5,e.fillStyle=t.textMuted;for(let a=0;a<=b;a++){const S=w+n*a/b,r=c+a/b*m;e.beginPath(),e.moveTo(r,s),e.lineTo(r,s+d),e.stroke(),e.textAlign="center",e.textBaseline="top",e.fillText(Y.tick(S),r,s+d+4);const v=M+o*a/b,u=s+(1-a/b)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="right",e.textBaseline="middle",e.fillText(K.tick(v),c-4,u)}Y.unitLabel&&We(e,Y.unitLabel,c+m/2,s+d+24,t.textMuted),K.unitLabel&&(e.save(),e.font=`${j(-1)}px system-ui, sans-serif`,e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.translate(6,s+d/2),e.rotate(-Math.PI/2),e.fillText(`(${K.unitLabel})`,0,0),e.restore());const P=h.size===0?f:f.filter(a=>h.has(I(a))),se=P.length>5e3?Math.ceil(P.length/5e3):1;e.globalAlpha=Math.max(.15,Math.min(.7,200/(P.length/se)));for(let a=0;a<P.length;a+=se){const S=P[a],r=c+(S.x-w)/n*m,v=s+(1-(S.y-M)/o)*d;e.beginPath(),e.arc(r,v,2.5,0,Math.PI*2),e.fillStyle=ne(I(S)),e.fill()}if(e.globalAlpha=1,p!==null&&x!==null){const a=z(p),S=z(x);e.strokeStyle=t.textMuted,e.fillStyle=t.textMuted,e.lineWidth=1,e.setLineDash([4,3]),e.font=`${j(-1)}px system-ui, sans-serif`,e.textBaseline="top";for(const[r,v]of[[a.limitLow,"LSL"],[a.limitHigh,"USL"]]){if(r===void 0||r<w||r>N)continue;const u=c+(r-w)/n*m;e.beginPath(),e.moveTo(u,s),e.lineTo(u,s+d),e.stroke(),e.textAlign="left",e.fillText(v,u+2,s+2)}for(const[r,v]of[[S.limitLow,"LSL"],[S.limitHigh,"USL"]]){if(r===void 0||r<M||r>R)continue;const u=s+(1-(r-M)/o)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="left",e.fillText(v,c+3,u+2)}e.setLineDash([])}e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,s),e.lineTo(c,s+d),e.moveTo(c,s+d),e.lineTo(c+m,s+d),e.stroke()}function $(){if(te(),F.length<2||p===null||x===null){f=[],le([]),G(),_();return}f=y?Me(E,p,x):we(Z(),p,x);let t;if(y){const n=new Set(f.map(I));t=V.filter(o=>n.has(o))}else t=Array.from(new Set(f.map(n=>n.hbin??0))).sort((n,o)=>n-o).map(String);if(le(t),G(),f.length>0){const n=f.map(b=>b.x),o=f.map(b=>b.y),l=Math.min(...n),g=Math.max(...n),m=Math.min(...o),d=Math.max(...o),B=(g-l)*.05||1,e=(d-m)*.05||1;w=l-B,N=g+B,M=m-e,R=d+e}_()}const ye=Ae(i,()=>_());$();function Se(t,n){p=t,x=n,ue.value=String(t),he.value=String(n),$()}return{card:i,setXY:Se,destroy:()=>ye.disconnect()}}
1
+ import{NO_DATA_FILL as ae}from"../../renderer/colorMap.js";import{maxOf as re,minOf as ce}from"../../core/utils.js";import{categorical as Me}from"./palette.js";import{buildScatterData as ve,buildScatterDataGrouped as Le}from"../../stats/scatter.js";import{pearsonOfPairs as ke}from"../../stats/correlation.js";import{buildFacetTable as Ce}from"../../stats/facets.js";import{SPACE as T,RADIUS as Ae,fontPx as j,FONT as q,CLR as H}from"../toolbar.js";import{cardShell as De,observeResize as Oe,makeTooltip as $e,attachChartTip as Be,makeTestSelect as Pe,makeWaferSelect as He,chartFillHeight as We,applyCanvasFlow as Ee,drawAxisUnit as Fe,resolveChartCanvasColors as Ie,makeAxisFormat as de,makeSeriesLegendItem as Ne,prepareCanvas as Re}from"./chartShell.js";const c=52,ze=16,s=16,_e=44;export function renderScatterPanel(W){const{title:me="Test scatter",items:C,testDefs:J,binColors:fe,onSaveImage:pe,groups:E}=W,{card:i,body:L,controlsRow:Q}=De(me,pe,W.ownerDocument),F=J.filter(t=>t.testNumber!==void 0),y=!!(E&&E.length>0),V=(E??[]).map(t=>t.key),ue=new Map(V.map((t,n)=>[t,n]));let p=W.xTestNumber??F[0]?.testNumber??null,x=W.yTestNumber??F[1]?.testNumber??p,A=null;const Z=()=>A!==null?C[A]?[C[A]]:[]:C;function ee(t,n,o){const l=i.ownerDocument.createElement("label");Object.assign(l.style,{display:"inline-flex",alignItems:"center",gap:T.xs,fontSize:q.body,color:H.label});const g=i.ownerDocument.createElement("span");g.textContent=t;const m=Pe(F,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:i.ownerDocument});return l.append(g,m),{wrap:l,select:m}}const{wrap:xe,select:he}=ee("X:",p,t=>{p=t,$()}),{wrap:ge,select:be}=ee("Y:",x,t=>{x=t,$()});Q.append(xe,ge),!y&&C.length>1&&Q.appendChild(He(C,A,t=>{A=t,$()},{ownerDocument:i.ownerDocument}));const D=i.ownerDocument.createElement("div");Object.assign(D.style,{color:H.warnText,background:H.warnBg,border:`1px solid ${H.warnBorder}`,borderRadius:Ae.control,padding:`${T.xs} ${T.md}`,fontSize:q.body,marginBottom:T.xs,display:"none"}),i.insertBefore(D,L);function te(){const t=y?[]:Ce(Z(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);t.length>0?(D.textContent=`\u26A0 Mixed ${t.join(", ")} across these points \u2014 trends here may be confounded (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,D.style.display=""):D.style.display="none"}te();const U=i.ownerDocument.createElement("div");Object.assign(U.style,{color:H.label,fontSize:q.body,marginBottom:T.xs}),i.insertBefore(U,L);function G(){const t=y?"One point per die \xB7 coloured by group \xB7 click legend to filter":"One point per die across all wafers \xB7 coloured by hard bin \xB7 click legend to filter",n=h.size===0?f:f.filter(g=>h.has(I(g))),{r:o,n:l}=ke(n);U.textContent=o===null?`${t} \xB7 n = ${l.toLocaleString()}`:`${t} \xB7 r = ${o.toFixed(3)} \xB7 n = ${l.toLocaleString()}`}const I=t=>y?t.group??"\u2014":String(t.hbin??0),ne=t=>y?Me(ue.get(t)??0):t==="0"?ae:fe?.get(Number(t))??ae,oe=t=>y?t:t==="0"?"No bin data":`Bin ${t}`,h=new Set,O=i.ownerDocument.createElement("div");Object.assign(O.style,{display:"flex",flexWrap:"wrap",gap:T.sm,marginTop:T.md,marginBottom:T.xl}),L.appendChild(O);const k=i.ownerDocument.createElement("canvas");k.style.display="block",k.style.cursor="crosshair",L.appendChild(k);const ye=$e(i);let f=[],w=0,N=1,M=0,R=1;function z(t){const n=J.find(o=>o.testNumber===t);return{unit:n?.unit,limitLow:n?.limitLow,limitHigh:n?.limitHigh}}function Se(){const t=L.clientWidth,n=Math.max(200,Math.min(400,t*.65)),o=We(i,L,k,n);return{w:t,h:o,plotW:Math.max(10,t-c-ze),plotH:Math.max(10,o-s-_e)}}const X=new Map;function ie(){for(const[t,n]of X){const o=h.has(t);n.setState({selected:o,dimmed:h.size>0&&!o})}}function le(t){O.innerHTML="",X.clear(),h.clear();for(const n of t){const o=Ne(oe(n),ne(n),i.ownerDocument),l=o.el;l.dataset.cat=n,X.set(n,o),Be(l,i,ye,`${oe(n)} \u2014 click to filter`),l.addEventListener("click",()=>{h.has(n)?h.delete(n):h.add(n),ie(),G(),_()}),O.appendChild(l)}ie()}function _(){Ee(k,O);const t=Ie(i),n=N-w,o=R-M,{w:l,h:g,plotW:m,plotH:d}=Se(),B=Re(k,i,l,g);if(!B)return;const{ctx:e}=B;if(f.length===0){e.font=`${j()}px system-ui, sans-serif`,e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.fillText("No data \u2014 select two parametric tests with values.",l/2,g/2);return}const b=4,Y=de(Math.max(Math.abs(w),Math.abs(N)),p!==null?z(p).unit:void 0),K=de(Math.max(Math.abs(M),Math.abs(R)),x!==null?z(x).unit:void 0);e.font=`${j(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=.5,e.fillStyle=t.textMuted;for(let a=0;a<=b;a++){const S=w+n*a/b,r=c+a/b*m;e.beginPath(),e.moveTo(r,s),e.lineTo(r,s+d),e.stroke(),e.textAlign="center",e.textBaseline="top",e.fillText(Y.tick(S),r,s+d+4);const v=M+o*a/b,u=s+(1-a/b)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="right",e.textBaseline="middle",e.fillText(K.tick(v),c-4,u)}Y.unitLabel&&Fe(e,Y.unitLabel,c+m/2,s+d+24,t.textMuted),K.unitLabel&&(e.save(),e.font=`${j(-1)}px system-ui, sans-serif`,e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.translate(6,s+d/2),e.rotate(-Math.PI/2),e.fillText(`(${K.unitLabel})`,0,0),e.restore());const P=h.size===0?f:f.filter(a=>h.has(I(a))),se=P.length>5e3?Math.ceil(P.length/5e3):1;e.globalAlpha=Math.max(.15,Math.min(.7,200/(P.length/se)));for(let a=0;a<P.length;a+=se){const S=P[a],r=c+(S.x-w)/n*m,v=s+(1-(S.y-M)/o)*d;e.beginPath(),e.arc(r,v,2.5,0,Math.PI*2),e.fillStyle=ne(I(S)),e.fill()}if(e.globalAlpha=1,p!==null&&x!==null){const a=z(p),S=z(x);e.strokeStyle=t.textMuted,e.fillStyle=t.textMuted,e.lineWidth=1,e.setLineDash([4,3]),e.font=`${j(-1)}px system-ui, sans-serif`,e.textBaseline="top";for(const[r,v]of[[a.limitLow,"LSL"],[a.limitHigh,"USL"]]){if(r===void 0||r<w||r>N)continue;const u=c+(r-w)/n*m;e.beginPath(),e.moveTo(u,s),e.lineTo(u,s+d),e.stroke(),e.textAlign="left",e.fillText(v,u+2,s+2)}for(const[r,v]of[[S.limitLow,"LSL"],[S.limitHigh,"USL"]]){if(r===void 0||r<M||r>R)continue;const u=s+(1-(r-M)/o)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="left",e.fillText(v,c+3,u+2)}e.setLineDash([])}e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,s),e.lineTo(c,s+d),e.moveTo(c,s+d),e.lineTo(c+m,s+d),e.stroke()}function $(){if(te(),F.length<2||p===null||x===null){f=[],le([]),G(),_();return}f=y?Le(E,p,x):ve(Z(),p,x);let t;if(y){const n=new Set(f.map(I));t=V.filter(o=>n.has(o))}else t=Array.from(new Set(f.map(n=>n.hbin??0))).sort((n,o)=>n-o).map(String);if(le(t),G(),f.length>0){const n=f.map(b=>b.x),o=f.map(b=>b.y),l=ce(n),g=re(n),m=ce(o),d=re(o),B=(g-l)*.05||1,e=(d-m)*.05||1;w=l-B,N=g+B,M=m-e,R=d+e}_()}const Te=Oe(i,()=>_());$();function we(t,n){p=t,x=n,he.value=String(t),be.value=String(n),$()}return{card:i,setXY:we,destroy:()=>Te.disconnect()}}
@@ -0,0 +1,45 @@
1
+ import type { Chunked } from '../core/utils.js';
2
+ /** Handle on a running {@link runChunked} computation. */
3
+ export interface ChunkedRun {
4
+ /** True once the work has finished — set synchronously when it finished
5
+ * inside the first slice, so a caller can tell "already done" from "staged". */
6
+ readonly done: boolean;
7
+ /**
8
+ * Abandon the rest of the work. Safe to call at any time, including after it
9
+ * has finished. Whatever the work has already written (DOM appended so far,
10
+ * for instance) is left as it is — the caller owns that, and in practice
11
+ * clears it by starting the next render.
12
+ */
13
+ cancel(): void;
14
+ }
15
+ /**
16
+ * Drive a {@link Chunked} computation on the main thread in time-boxed slices,
17
+ * yielding to the event loop between them so the browser can paint, keep
18
+ * responding to input, and — the reason this exists — never show its "page
19
+ * unresponsive" dialog.
20
+ *
21
+ * **The first slice is deliberately generous.** Work that finishes inside it
22
+ * never yields at all, so a small lot's panel renders in one synchronous pass
23
+ * exactly as it always did: no flash of half a panel, no extra frame, nothing
24
+ * for a caller to special-case. Only work that genuinely overruns starts
25
+ * staging, which is the case the staging is for.
26
+ *
27
+ * A slice always runs at least one step before checking its budget, because a
28
+ * step is atomic — the budget bounds how many steps share a task, never how
29
+ * long one step may take. **Keeping each step short is the generator's job**,
30
+ * and it is what actually bounds the longest task.
31
+ *
32
+ * `setTimeout(0)`, not `requestAnimationFrame`: a slice may overrun a frame, and
33
+ * rAF in a background tab stops firing entirely, which would strand a render.
34
+ */
35
+ export declare function runChunked<T>(work: Chunked<T>, opts?: {
36
+ /** Budget for the synchronous first slice. Default 150 ms — below the ~250 ms
37
+ * at which a wait starts reading as a stall, so anything under it is better
38
+ * finished than staged. Pass 0 to force staging (what the tests do). */
39
+ firstSliceMs?: number;
40
+ /** Budget for every slice after the first. Default 16 ms — one frame. */
41
+ sliceMs?: number;
42
+ /** Called with the result when the work finishes. Not called if cancelled. */
43
+ onDone?: (result: T) => void;
44
+ }): ChunkedRun;
45
+ //# sourceMappingURL=chunked.d.ts.map
@@ -0,0 +1 @@
1
+ function f(){return typeof performance<"u"?performance.now():Date.now()}export function runChunked(r,i={}){const{firstSliceMs:c=150,sliceMs:d=16,onDone:s}=i;let n=!1,t=!1,e;const u=l=>{if(e=void 0,n)return;const a=f();for(;;){const o=r.next();if(o.done){t=!0,s?.(o.value);return}if(f()-a>=l)break}e=setTimeout(()=>u(d),0)};return u(c),{get done(){return t},cancel(){n||t||(n=!0,e!==void 0&&(clearTimeout(e),e=void 0),r.return(void 0))}}}