@wafertools/wafermap 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +16 -5
  2. package/CHANGELOG.md +101 -0
  3. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  4. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  5. package/dist/packages/canvas-adapter/dieList.d.ts +41 -0
  6. package/dist/packages/canvas-adapter/dieList.js +2 -0
  7. package/dist/packages/canvas-adapter/index.d.ts +2 -0
  8. package/dist/packages/canvas-adapter/index.js +1 -1
  9. package/dist/packages/canvas-adapter/maplessSummary.d.ts +34 -0
  10. package/dist/packages/canvas-adapter/maplessSummary.js +1 -0
  11. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +2 -18
  12. package/dist/packages/canvas-adapter/renderWaferGallery.js +8 -8
  13. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +17 -1
  14. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  15. package/dist/packages/canvas-adapter/summaryPanel.d.ts +2 -0
  16. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  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/canvas-adapter/warnings.d.ts +6 -1
  22. package/dist/packages/core/classify.d.ts +6 -2
  23. package/dist/packages/core/dies.d.ts +67 -7
  24. package/dist/packages/core/dies.js +1 -1
  25. package/dist/packages/core/probe.d.ts +5 -0
  26. package/dist/packages/core/probe.js +1 -1
  27. package/dist/packages/core/transforms.d.ts +4 -4
  28. package/dist/packages/core/transforms.js +1 -1
  29. package/dist/packages/renderer/buildView.d.ts +3 -3
  30. package/dist/packages/renderer/buildView.js +1 -1
  31. package/dist/packages/renderer/buildWaferMap.d.ts +25 -8
  32. package/dist/packages/renderer/buildWaferMap.js +1 -1
  33. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  34. package/dist/packages/stats/clusterDetection.d.ts +2 -2
  35. package/dist/packages/stats/patternClassification.d.ts +2 -2
  36. package/dist/packages/stats/regions.d.ts +6 -6
  37. package/dist/packages/stats/regions.js +1 -1
  38. package/dist/packages/stats/renderSummaryReport.js +17 -17
  39. package/llms.txt +1 -1
  40. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -53,15 +53,26 @@ correctness guarantee that comes with it.
53
53
  sites that fit. Never recompute a `partial` flag by testing die corners against
54
54
  the wafer circle — that manufactures fake partial dies which are then greyed out
55
55
  and dropped from yield. A die outside the wafer means the *geometry* is wrong.
56
- - **Check `result.warnings` and `summary.stats.warnings`.** Both are
57
- `WaferWarning[]` — `{ code, message, severity }`. Branch on `code`, never on the
58
- prose. Geometry advisories are severity `'error'`: they mean dies may be drawn in
56
+ - **Check `result.warnings` and `summary.stats.warnings`.** Both carry
57
+ `WaferWarning` — `{ code, message, severity }`. Branch on `code`, never on the
58
+ prose. **The two differ in shape and the difference throws:**
59
+ `result.warnings` is required and always an array (`[]` when clean), but
60
+ `summary.stats.warnings` is *optional* and is `undefined` when there is nothing
61
+ to report. Write `summary.stats.warnings?.length` — a bare
62
+ `summary.stats.warnings.length` is a TypeError on every clean wafer, which is
63
+ most of them, so it will pass your testing and fail in production.
64
+ Geometry advisories are severity `'error'`: they mean dies may be drawn in
59
65
  the wrong place. The renderers surface these themselves in a toolbar indicator, so
60
66
  do NOT hand-roll a second display — pass
61
67
  `warnings: { display: false, onWarning }` if the app has its own notification UI.
62
68
  (`result.inference.warnings` is a deprecated string mirror; do not use it.)
63
- - **Container needs a real height.** A zero-height parent renders nothing. This is
64
- the most common "it didn't work" report.
69
+ - **Give the container a resolved height.** `renderWaferMap` fills its container.
70
+ A bare block-flow `<div>` is fine — it grows to the canvas. The real failure is a
71
+ flex/grid child whose ancestors never resolve a height: it stays 0-tall and the
72
+ map is invisible. The library detects exactly that case after layout settles and
73
+ `console.warn`s with the fix, so read the console before debugging further. Either
74
+ give the container a real CSS height, or pass `{ height: 600 }` in the render
75
+ options and the library will size it for you.
65
76
 
66
77
  ### API facts that are easy to guess wrong
67
78
 
package/CHANGELOG.md CHANGED
@@ -19,6 +19,107 @@ under `### Breaking`.
19
19
 
20
20
  ---
21
21
 
22
+ ## [0.23.0] — 2026-08-16
23
+
24
+ ### Breaking
25
+
26
+ - **`Die.x`/`y`/`physX`/`physY` are now optional**, not `number`. Existing code
27
+ that reads `die.x` as a bare number (e.g. arithmetic without a guard) now gets
28
+ a type error under strict TS — the runtime value for already-positioned data
29
+ is unchanged, only the type widened. New `isPositionedDie(die): die is PositionedDie`
30
+ (`@wafertools/wafermap/core`) narrows it back; every spatial function in the
31
+ library (region builders, cluster/pattern detection, `buildView`) now takes
32
+ `PositionedDie[]` rather than `Die[]`, so a caller passing unfiltered `Die[]`
33
+ into one of these will see the type error at the call site rather than a
34
+ runtime surprise. Its sibling `hasPosition(die)` is the looser guard for
35
+ inputs that carry `x`/`y` but no `physX`/`physY` (a raw `DieResult`, before
36
+ layout); use `isPositionedDie` for anything holding built `Die`s.
37
+
38
+ ### Added
39
+
40
+ - **`renderWaferMap` accepts a `RenderableWaferMap`** — `{ wafer, dies }` plus
41
+ every other `WaferMapResult` field as optional — rather than requiring a full
42
+ `WaferMapResult`. A `WaferMapResult` still satisfies it, so no caller needs to
43
+ change. This makes the gallery's own use honest: it renders each card through
44
+ `renderWaferMap`, but a card is a `WaferMapDisplayItem` carrying no
45
+ `dataCoverage`/`viewport`/`legendBox`/`binLegendRows`/`reticleConfig`, and it
46
+ used to be passed via `item as WaferMapResult`. That cast asserted fields
47
+ which were absent at runtime; reading one unguarded threw, and each such read
48
+ had to be found by hitting it. `collectWarnings` likewise now types
49
+ `result.warnings` as optional, which its body already handled.
50
+
51
+ - **Support for dies/wafers with no reported X/Y position.** Real-world data
52
+ sometimes has no spatial layout at all (wafer-number-only test logs), or a
53
+ lot/wafer where some dies report a position and others don't — previously
54
+ such a die was silently dropped by every parser upstream of this library, and
55
+ wmap itself had no representation for "no position" beyond crashing or
56
+ drawing garbage.
57
+ - `hasPosition(die)` / `isPositionedDie(die)` (`packages/core/dies.ts`) are
58
+ the predicates every spatial code path filters on — the first narrows
59
+ `x`/`y`, the second narrows `physX`/`physY` with them and so yields a
60
+ `PositionedDie`. `getDieKey` falls back to `` `id:${id}` `` for an
61
+ unpositioned die so two of them never collide.
62
+ - `buildWaferMap` partitions positioned/unpositioned dies before geometry
63
+ inference runs; unpositioned dies are folded back into the returned
64
+ `dies`/`dataCoverage`/`yield` afterward. `dataCoverage` gained
65
+ `unpositionedDies: number` (always present); `totalDies`/`filledDies`/
66
+ `ratio` stay scoped to positioned dies only. Yield is deliberately
67
+ **not** spatial — a coordinate-less die with bin data still counts toward
68
+ it, same as `isYieldEligibleDie` always ignored position.
69
+ - `analyzeWaferMap`/`regions.ts` exclude unpositioned dies from spatial
70
+ region families only (ring/quadrant/sector/reticle-position, cluster and
71
+ pattern detection). Everything value-based (yield, bin counts, per-test
72
+ stats, spec-limit yield) is unaffected — coordinate-less dies still count.
73
+ - A wafer with **zero** positioned dies never renders as a wafer-shaped
74
+ visual — a fabricated mosaic risks being misread as real spatial data.
75
+ `renderWaferMap`/`renderWaferGallery` instead show a compact,
76
+ plot-mode-aware summary in its place: a bin breakdown (reusing the
77
+ existing summary-panel component, so colours match the map's own bin
78
+ legend) for hard/soft-bin modes, or a small histogram for value mode
79
+ (coloured through the same colour-scheme/log-scale/spec-range resolution
80
+ the map's own colorbar uses). A **View die list** toggle reaches the full
81
+ per-die table (`buildDieListSection`, new, exported from `/render`) for
82
+ CSV export and per-die inspection, and **View chart** returns. A mixed
83
+ wafer keeps its normal map for the positioned dies, plus an expandable
84
+ "+N dies without position data" footer giving the same chart/die-list
85
+ toggle for the rest.
86
+ - The toolbar's spatial-only controls (zoom/pan/select/download,
87
+ orientation, overlays, legend position) are hidden on a fully
88
+ coordinate-less card — nothing to act on with no map drawn. Plot mode and
89
+ colour scheme stay, since both drive the summary's own appearance.
90
+ - A gallery card's own "expand" button now preserves that card's live view
91
+ state (plot mode, active test, colour scheme, …) when opening the
92
+ detached window/popup, rather than always reverting to the gallery's
93
+ shared default — a pre-existing gap for any card (not specific to
94
+ coordinate-less data) that became far more visible once a card's
95
+ representation could change shape between modes (bin breakdown vs.
96
+ histogram vs. map).
97
+
98
+ ### Fixed
99
+
100
+ - **`buildDieListSection`'s table was capped at a fixed 360px**, regardless of
101
+ how much room its container actually had — a short table floated in a
102
+ half-empty card, and a host mounting it inside a scrolling container of its
103
+ own got two nested scrollbars. It now fills its container by default
104
+ (`flex: 1; min-height: 0`); a new optional `maxHeight` re-imposes a cap for a
105
+ host with no definite-height container to fill.
106
+
107
+ ### Known gaps (not fixed this pass, tracked for a follow-up)
108
+
109
+ - Stacked plot modes (`stackedValues`/`stackedBins`/`stackedSoftBins`) discard
110
+ every individual wafer card from the gallery grid and pool across the lot —
111
+ a coordinate-less wafer's dies are correctly excluded from those pooled
112
+ aggregates (stacking is inherently position-based), but nothing in the UI
113
+ currently indicates a wafer was excluded; it's indistinguishable from that
114
+ wafer not existing in the lot at all.
115
+ - `hasPosition`, `PositionedDie`, `dataCoverage.unpositionedDies`, and
116
+ `buildDieListSection`/`DieListOptions` are documented in `docs/api.md`
117
+ (§12.1, §7); a dedicated numbered reference entry for `hasPosition` and
118
+ `buildDieListSection` alongside the library's other numbered API entries
119
+ has not been added yet.
120
+
121
+ ---
122
+
22
123
  ## [0.22.0] — 2026-08-04
23
124
 
24
125
  ### Breaking
@@ -1 +1 @@
1
- import{buildTestHistogramData as Ht,buildTestHistogramSeries as At}from"../../stats/histogram.js";import{CLR as lt}from"../toolbar.js";import{fmt as st}from"../../renderer/fmt.js";import{QUANTITY as Wt,categorical as Ct}from"./palette.js";import{cardShell as Et,observeResize as xt,makeTooltip as Pt,positionChartTooltip as bt,makeTestSelect as $t,makeWaferSelect as Bt,makeToggle as Rt,renderEmptyState as pt,chartFillHeight as yt,applyCanvasFlow as Mt,resolveChartCanvasColors as Tt,makeAxisFormat as vt,PADDING as tt}from"./chartShell.js";const mt=230,St=36,at=18;function kt(s,R,N,z,rt,I,b,u=4){const j=Math.max(1,I/u),F=Math.pow(10,Math.floor(Math.log10(j))),f=j/F,G=(f<=1?1:f<=2?2:f<=5?5:10)*F,$=z-N;s.save(),s.font="10px system-ui, sans-serif",s.textAlign="right",s.textBaseline="middle";for(let K=0;K<=I+1e-9;K+=G){const _=z-K/(I||1)*$;s.strokeStyle=b.grid,s.lineWidth=1,s.beginPath(),s.moveTo(R,_),s.lineTo(R+rt,_),s.stroke(),s.fillStyle=b.text,s.fillText(String(Math.round(K)),R-4,_)}s.strokeStyle=b.axis,s.lineWidth=1,s.beginPath(),s.moveTo(R,N),s.lineTo(R,z),s.stroke(),s.restore()}export function renderHistogramPanel(s){const{title:R="Test value distribution",items:N,testDefs:z,onSaveImage:rt,groups:I}=s,{card:b,body:u,controlsRow:j}=Et(R,rt),F=z.filter(a=>a.testNumber!==void 0);let f=s.selectedTestNumber??F[0]?.testNumber??null,G=null,$=!1;const K=$t(F,f,a=>{f=a,U()},{maxWidth:"200px"});j.appendChild(K);const _=Bt(N,G,a=>{G=a,U()});j.appendChild(_),j.appendChild(Rt("Axis includes limits",$,a=>{$=a,U()}));const A=Pt(b);let J=null,M=null;function et(a){const w=z.find(O=>O.testNumber===a);return{unit:w?.unit,limitLow:w?.limitLow,limitHigh:w?.limitHigh}}function U(){if(J?.disconnect(),J=null,u.innerHTML="",f===null){pt(u,"No parametric test data available for a histogram.");return}const a=I&&I.length>0?At(I,f,16,$?et(f).limitLow:void 0,$?et(f).limitHigh:void 0):null;if(_.style.display=a?"none":"",a){if(a.series.length===0){pt(u,"No parametric test data available for a histogram.");return}Lt(a);return}const w=G!==null?[N[G]]:N,{unit:O,limitLow:nt,limitHigh:x}=et(f),i=Ht(w,f,16,$?nt:void 0,$?x:void 0);if(i.length===0){pt(u,"No parametric test data available for a histogram.");return}const D=Math.max(...i.map(p=>p.count),1),P=document.createElement("div");Object.assign(P.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),P.textContent=`max ${D} dies/bucket`,u.appendChild(P);const h=document.createElement("canvas");h.style.display="block",h.style.cursor="default",u.appendChild(h);let v=-1;const g=window.devicePixelRatio||1,B=i[0].rangeLow,X=i[i.length-1].rangeHigh,ot=X-B||1,Z=vt(Math.max(Math.abs(B),Math.abs(X)),O);function Y(p){const m=tt+36,e=h.clientWidth-m-tt,n=p-St-at;return{plotX:m,plotMaxWidth:Math.max(10,e),plotMaxHeight:n,plotTop:at}}function ct(p){const{plotX:m,plotMaxWidth:e}=Y(mt);if(p<m||p>m+e)return-1;const n=Math.floor((p-m)/e*i.length);return n>=0&&n<i.length?n:-1}function H(){Mt(h,P.offsetHeight);const p=u.clientWidth,m=yt(b,u,h,mt);h.width=Math.max(1,Math.floor(p*g)),h.height=Math.max(1,Math.floor(m*g)),h.style.width=`${p}px`,h.style.height=`${m}px`;const e=h.getContext("2d");e.setTransform(g,0,0,g,0,0),e.clearRect(0,0,p,m),e.font="11px system-ui, sans-serif";const n=Tt(b),{plotX:c,plotMaxWidth:t,plotMaxHeight:d,plotTop:o}=Y(m),l=o+d,S=t/i.length;kt(e,c,o,l,t,D,{text:n.textMuted,axis:n.border,grid:n.border}),i.forEach((y,C)=>{const k=y.count/D*d,r=c+C*S,L=l-k;e.fillStyle=C===v?n.text:Wt,e.globalAlpha=C===v?1:.7,e.fillRect(r+1,L,Math.max(1,S-2),k),e.globalAlpha=1,C===v&&(e.fillStyle=n.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${y.count}`,r+S/2,L-2))});const V=y=>c+(y-B)/ot*t;e.font="10px system-ui, sans-serif";for(const[y,C]of[[nt,"LSL"],[x,"USL"]]){if(y===void 0)continue;const k=V(y);e.strokeStyle=n.warnBorder,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(k,o),e.lineTo(k,l),e.stroke(),e.setLineDash([]),e.fillStyle=n.warnBorder,e.textAlign="center",e.textBaseline="bottom",e.fillText(C,k,o-2)}e.font="11px system-ui, sans-serif",e.strokeStyle=n.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,l),e.lineTo(c+t,l),e.stroke(),e.fillStyle=n.textMuted,e.textAlign="center",e.textBaseline="top";const W=Math.max(2,Math.floor(t/44)),dt=Math.ceil(i.length/W),ht=Math.max(1,dt);for(let y=0;y<=i.length;y+=ht){const C=y<i.length?i[y].rangeLow:i[i.length-1].rangeHigh,k=c+y*S;e.beginPath(),e.moveTo(k,l),e.lineTo(k,l+4),e.stroke(),e.fillText(Z.tick(C),k,l+6)}Z.unitLabel&&(e.save(),e.font="10px system-ui, sans-serif",e.fillStyle=n.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${Z.unitLabel})`,c+t,l+20),e.restore())}h.addEventListener("mousemove",p=>{const m=h.getBoundingClientRect(),e=ct(p.clientX-m.left);if(e!==v&&(v=e,H()),e>=0){const n=i[e];A.innerHTML=`<strong>${st(n.rangeLow,O,"engineering")} \u2013 ${st(n.rangeHigh,O,"engineering")}</strong><br>${n.count} dies`,A.style.display="block",bt(A,b,p.clientX,p.clientY)}else A.style.display="none"}),h.addEventListener("mouseleave",()=>{v!==-1&&(v=-1,H()),A.style.display="none"}),J=xt(b,()=>H()),H()}function Lt(a){const{unit:w,limitLow:O,limitHigh:nt}=et(f),x=a.ranges,i=a.series,D=Ct;M&&!i.some(n=>n.groupKey===M)&&(M=null);const P=Math.max(1,...i.flatMap(n=>n.counts)),h=document.createElement("div");Object.assign(h.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),h.textContent=`${i.length} groups \xB7 max ${P} dies/bucket`,u.appendChild(h);const v=document.createElement("div");Object.assign(v.style,{display:"flex",flexWrap:"wrap",gap:"4px 12px",marginBottom:"4px"}),i.forEach((n,c)=>{const t=document.createElement("button");t.type="button",t.title=`${n.groupKey} \u2014 click to emphasize (dim the rest)`,Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:"5px",fontSize:"11px",padding:"1px 4px",border:"none",background:"none",cursor:"pointer",color:lt.text,borderRadius:"3px"});const d=document.createElement("span");Object.assign(d.style,{width:"10px",height:"10px",borderRadius:"2px",background:D(c),flex:"0 0 auto"});const o=document.createElement("span");o.textContent=n.groupKey,t.append(d,o);const l=M!==null&&M!==n.groupKey;t.style.opacity=l?"0.45":"1",M===n.groupKey&&(t.style.background=lt.bgHover),t.addEventListener("click",()=>{M=M===n.groupKey?null:n.groupKey,U()}),v.appendChild(t)}),u.appendChild(v);const g=document.createElement("canvas");g.style.display="block",g.style.cursor="default",u.appendChild(g);const B=window.devicePixelRatio||1,X=x[0].rangeLow,ot=x[x.length-1].rangeHigh,Z=ot-X||1,Y=vt(Math.max(Math.abs(X),Math.abs(ot)),w),ct=()=>h.offsetHeight+v.offsetHeight;let H=-1,p={plotX:tt+36,plotMaxWidth:10,barWidth:10};function m(){Mt(g,ct());const n=u.clientWidth,c=yt(b,u,g,mt);g.width=Math.max(1,Math.floor(n*B)),g.height=Math.max(1,Math.floor(c*B)),g.style.width=`${n}px`,g.style.height=`${c}px`;const t=g.getContext("2d");t.setTransform(B,0,0,B,0,0),t.clearRect(0,0,n,c),t.font="11px system-ui, sans-serif";const d=Tt(b),o=tt+36,l=Math.max(10,n-o-tt),S=at,V=c-St-at,T=S+V,W=l/x.length;p={plotX:o,plotMaxWidth:l,barWidth:W},kt(t,o,S,T,l,P,{text:d.textMuted,axis:d.border,grid:d.border}),H>=0&&(t.fillStyle=d.bgHover,t.globalAlpha=.5,t.fillRect(o+H*W,S,W,V),t.globalAlpha=1);const dt=i.map((r,L)=>({s:r,i:L})).sort((r,L)=>(r.s.groupKey===M?1:0)-(L.s.groupKey===M?1:0));for(const{s:r,i:L}of dt){const E=M===r.groupKey,ut=M!==null&&!E,ft=D(L);t.strokeStyle=ft,t.fillStyle=ft,t.lineWidth=E?2:1,t.globalAlpha=ut?.12:M?.32:.22,t.beginPath(),t.moveTo(o,T),r.counts.forEach((gt,it)=>{const Q=o+it*W,q=T-gt/P*V;t.lineTo(Q,q),t.lineTo(Q+W,q)}),t.lineTo(o+l,T),t.closePath(),t.fill(),t.globalAlpha=ut?.3:1,t.beginPath(),r.counts.forEach((gt,it)=>{const Q=o+it*W,q=T-gt/P*V;it===0?t.moveTo(Q,q):t.lineTo(Q,q),t.lineTo(Q+W,q)}),t.stroke()}t.globalAlpha=1;const ht=r=>o+(r-X)/Z*l;t.font="10px system-ui, sans-serif";for(const[r,L]of[[O,"LSL"],[nt,"USL"]]){if(r===void 0)continue;const E=ht(r);t.strokeStyle=d.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(E,S),t.lineTo(E,T),t.stroke(),t.setLineDash([]),t.fillStyle=d.warnBorder,t.textAlign="center",t.textBaseline="bottom",t.fillText(L,E,S-2)}t.font="11px system-ui, sans-serif",t.strokeStyle=d.border,t.lineWidth=1,t.beginPath(),t.moveTo(o,T),t.lineTo(o+l,T),t.stroke(),t.fillStyle=d.textMuted,t.textAlign="center",t.textBaseline="top";const C=Math.max(2,Math.floor(l/44)),k=Math.max(1,Math.ceil(x.length/C));for(let r=0;r<=x.length;r+=k){const L=r<x.length?x[r].rangeLow:x[x.length-1].rangeHigh,E=o+r*W;t.beginPath(),t.moveTo(E,T),t.lineTo(E,T+4),t.stroke(),t.fillText(Y.tick(L),E,T+6)}Y.unitLabel&&(t.save(),t.font="10px system-ui, sans-serif",t.fillStyle=d.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${Y.unitLabel})`,o+l,T+20),t.restore())}function e(n){const{plotX:c,plotMaxWidth:t,barWidth:d}=p;if(n<c||n>c+t)return-1;const o=Math.floor((n-c)/d);return o>=0&&o<x.length?o:-1}g.addEventListener("mousemove",n=>{const c=g.getBoundingClientRect(),t=e(n.clientX-c.left);if(t!==H&&(H=t,m()),t>=0){const d=x[t],o=i.map((l,S)=>`<span style="display:inline-block;width:9px;height:9px;border-radius:2px;background:${D(S)};margin-right:5px;"></span>${l.groupKey}: ${l.counts[t]}`).join("<br>");A.innerHTML=`<strong>${st(d.rangeLow,w,"engineering")} \u2013 ${st(d.rangeHigh,w,"engineering")}</strong><br>${o}`,A.style.display="block",bt(A,b,n.clientX,n.clientY)}else A.style.display="none"}),g.addEventListener("mouseleave",()=>{H!==-1&&(H=-1,m()),A.style.display="none"}),J=xt(b,()=>m()),m()}U();function wt(a){!F.some(w=>w.testNumber===a)||a===f||(f=a,K.value=String(a),U())}return{card:b,setTest:wt,destroy:()=>J?.disconnect()}}
1
+ import{buildTestHistogramData as Ht,buildTestHistogramSeries as At}from"../../stats/histogram.js";import{CLR as lt}from"../toolbar.js";import{fmt as st}from"../../renderer/fmt.js";import{QUANTITY as Wt,categorical as Ct}from"./palette.js";import{cardShell as Et,observeResize as xt,makeTooltip as Pt,positionChartTooltip as bt,makeTestSelect as $t,makeWaferSelect as Bt,makeToggle as Rt,renderEmptyState as pt,chartFillHeight as yt,applyCanvasFlow as Mt,resolveChartCanvasColors as Tt,makeAxisFormat as vt,PADDING as tt}from"./chartShell.js";const mt=230,St=36,at=18;function kt(s,R,N,z,rt,I,b,u=4){const j=Math.max(1,I/u),F=Math.pow(10,Math.floor(Math.log10(j))),f=j/F,G=(f<=1?1:f<=2?2:f<=5?5:10)*F,$=z-N;s.save(),s.font="10px system-ui, sans-serif",s.textAlign="right",s.textBaseline="middle";for(let K=0;K<=I+1e-9;K+=G){const _=z-K/(I||1)*$;s.strokeStyle=b.grid,s.lineWidth=1,s.beginPath(),s.moveTo(R,_),s.lineTo(R+rt,_),s.stroke(),s.fillStyle=b.text,s.fillText(String(Math.round(K)),R-4,_)}s.strokeStyle=b.axis,s.lineWidth=1,s.beginPath(),s.moveTo(R,N),s.lineTo(R,z),s.stroke(),s.restore()}export function renderHistogramPanel(s){const{title:R="Test value distribution",items:N,testDefs:z,onSaveImage:rt,groups:I}=s,{card:b,body:u,controlsRow:j}=Et(R,rt),F=z.filter(a=>a.testNumber!==void 0);let f=s.selectedTestNumber??F[0]?.testNumber??null,G=null,$=!1;const K=$t(F,f,a=>{f=a,U()},{maxWidth:"200px"});j.appendChild(K);const _=Bt(N,G,a=>{G=a,U()});j.appendChild(_),j.appendChild(Rt("Axis includes limits",$,a=>{$=a,U()}));const A=Pt(b);let J=null,y=null;function et(a){const w=z.find(O=>O.testNumber===a);return{unit:w?.unit,limitLow:w?.limitLow,limitHigh:w?.limitHigh}}function U(){if(J?.disconnect(),J=null,u.innerHTML="",f===null){pt(u,"No parametric test data available for a histogram.");return}const a=I&&I.length>0?At(I,f,16,$?et(f).limitLow:void 0,$?et(f).limitHigh:void 0):null;if(_.style.display=a?"none":"",a){if(a.series.length===0){pt(u,"No parametric test data available for a histogram.");return}Lt(a);return}const w=G!==null?[N[G]]:N,{unit:O,limitLow:nt,limitHigh:x}=et(f),i=Ht(w,f,16,$?nt:void 0,$?x:void 0);if(i.length===0){pt(u,"No parametric test data available for a histogram.");return}const D=Math.max(...i.map(p=>p.count),1),P=document.createElement("div");Object.assign(P.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),P.textContent=`max ${D} dies/bucket`,u.appendChild(P);const h=document.createElement("canvas");h.style.display="block",h.style.cursor="default",u.appendChild(h);let v=-1;const g=window.devicePixelRatio||1,B=i[0].rangeLow,X=i[i.length-1].rangeHigh,ot=X-B||1,Z=vt(Math.max(Math.abs(B),Math.abs(X)),O);function Y(p){const m=tt+36,e=h.clientWidth-m-tt,n=p-St-at;return{plotX:m,plotMaxWidth:Math.max(10,e),plotMaxHeight:n,plotTop:at}}function ct(p){const{plotX:m,plotMaxWidth:e}=Y(mt);if(p<m||p>m+e)return-1;const n=Math.floor((p-m)/e*i.length);return n>=0&&n<i.length?n:-1}function H(){Mt(h,P.offsetHeight);const p=u.clientWidth,m=yt(b,u,h,mt);h.width=Math.max(1,Math.floor(p*g)),h.height=Math.max(1,Math.floor(m*g)),h.style.width=`${p}px`,h.style.height=`${m}px`;const e=h.getContext("2d");e.setTransform(g,0,0,g,0,0),e.clearRect(0,0,p,m),e.font="11px system-ui, sans-serif";const n=Tt(b),{plotX:c,plotMaxWidth:t,plotMaxHeight:d,plotTop:o}=Y(m),l=o+d,S=t/i.length;kt(e,c,o,l,t,D,{text:n.textMuted,axis:n.border,grid:n.border}),i.forEach((M,C)=>{const k=M.count/D*d,r=c+C*S,L=l-k;e.fillStyle=C===v?n.text:Wt,e.globalAlpha=C===v?1:.7,e.fillRect(r+1,L,Math.max(1,S-2),k),e.globalAlpha=1,C===v&&(e.fillStyle=n.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${M.count}`,r+S/2,L-2))});const V=M=>c+(M-B)/ot*t;e.font="10px system-ui, sans-serif";for(const[M,C]of[[nt,"LSL"],[x,"USL"]]){if(M===void 0)continue;const k=V(M);e.strokeStyle=n.warnBorder,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(k,o),e.lineTo(k,l),e.stroke(),e.setLineDash([]),e.fillStyle=n.warnBorder,e.textAlign="center",e.textBaseline="bottom",e.fillText(C,k,o-2)}e.font="11px system-ui, sans-serif",e.strokeStyle=n.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,l),e.lineTo(c+t,l),e.stroke(),e.fillStyle=n.textMuted,e.textAlign="center",e.textBaseline="top";const W=Math.max(2,Math.floor(t/44)),dt=Math.ceil(i.length/W),ht=Math.max(1,dt);for(let M=0;M<=i.length;M+=ht){const C=M<i.length?i[M].rangeLow:i[i.length-1].rangeHigh,k=c+M*S;e.beginPath(),e.moveTo(k,l),e.lineTo(k,l+4),e.stroke(),e.fillText(Z.tick(C),k,l+6)}Z.unitLabel&&(e.save(),e.font="10px system-ui, sans-serif",e.fillStyle=n.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${Z.unitLabel})`,c+t,l+20),e.restore())}h.addEventListener("mousemove",p=>{const m=h.getBoundingClientRect(),e=ct(p.clientX-m.left);if(e!==v&&(v=e,H()),e>=0){const n=i[e];A.innerHTML=`<strong>${st(n.rangeLow,O,"engineering")} \u2013 ${st(n.rangeHigh,O,"engineering")}</strong><br>${n.count} dies`,A.style.display="block",bt(A,b,p.clientX,p.clientY)}else A.style.display="none"}),h.addEventListener("mouseleave",()=>{v!==-1&&(v=-1,H()),A.style.display="none"}),J=xt(b,()=>H()),H()}function Lt(a){const{unit:w,limitLow:O,limitHigh:nt}=et(f),x=a.ranges,i=a.series,D=Ct;y&&!i.some(n=>n.groupKey===y)&&(y=null);const P=Math.max(1,...i.flatMap(n=>n.counts)),h=document.createElement("div");Object.assign(h.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),h.textContent=`${i.length} groups \xB7 max ${P} dies/bucket`,u.appendChild(h);const v=document.createElement("div");Object.assign(v.style,{display:"flex",flexWrap:"wrap",gap:"4px 12px",marginBottom:"4px"}),i.forEach((n,c)=>{const t=document.createElement("button");t.type="button",t.title=`${n.groupKey} \u2014 click to emphasize (dim the rest)`,Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:"5px",fontSize:"11px",padding:"1px 4px",border:"none",background:"none",cursor:"pointer",color:lt.text,borderRadius:"3px"});const d=document.createElement("span");Object.assign(d.style,{width:"10px",height:"10px",borderRadius:"2px",background:D(c),flex:"0 0 auto"});const o=document.createElement("span");o.textContent=n.groupKey,t.append(d,o);const l=y!==null&&y!==n.groupKey;t.style.opacity=l?"0.45":"1",y===n.groupKey&&(t.style.background=lt.bgHover),t.setAttribute("aria-pressed",y===n.groupKey?"true":"false"),t.addEventListener("click",()=>{y=y===n.groupKey?null:n.groupKey,U()}),v.appendChild(t)}),u.appendChild(v);const g=document.createElement("canvas");g.style.display="block",g.style.cursor="default",u.appendChild(g);const B=window.devicePixelRatio||1,X=x[0].rangeLow,ot=x[x.length-1].rangeHigh,Z=ot-X||1,Y=vt(Math.max(Math.abs(X),Math.abs(ot)),w),ct=()=>h.offsetHeight+v.offsetHeight;let H=-1,p={plotX:tt+36,plotMaxWidth:10,barWidth:10};function m(){Mt(g,ct());const n=u.clientWidth,c=yt(b,u,g,mt);g.width=Math.max(1,Math.floor(n*B)),g.height=Math.max(1,Math.floor(c*B)),g.style.width=`${n}px`,g.style.height=`${c}px`;const t=g.getContext("2d");t.setTransform(B,0,0,B,0,0),t.clearRect(0,0,n,c),t.font="11px system-ui, sans-serif";const d=Tt(b),o=tt+36,l=Math.max(10,n-o-tt),S=at,V=c-St-at,T=S+V,W=l/x.length;p={plotX:o,plotMaxWidth:l,barWidth:W},kt(t,o,S,T,l,P,{text:d.textMuted,axis:d.border,grid:d.border}),H>=0&&(t.fillStyle=d.bgHover,t.globalAlpha=.5,t.fillRect(o+H*W,S,W,V),t.globalAlpha=1);const dt=i.map((r,L)=>({s:r,i:L})).sort((r,L)=>(r.s.groupKey===y?1:0)-(L.s.groupKey===y?1:0));for(const{s:r,i:L}of dt){const E=y===r.groupKey,ut=y!==null&&!E,ft=D(L);t.strokeStyle=ft,t.fillStyle=ft,t.lineWidth=E?2:1,t.globalAlpha=ut?.12:y?.32:.22,t.beginPath(),t.moveTo(o,T),r.counts.forEach((gt,it)=>{const Q=o+it*W,q=T-gt/P*V;t.lineTo(Q,q),t.lineTo(Q+W,q)}),t.lineTo(o+l,T),t.closePath(),t.fill(),t.globalAlpha=ut?.3:1,t.beginPath(),r.counts.forEach((gt,it)=>{const Q=o+it*W,q=T-gt/P*V;it===0?t.moveTo(Q,q):t.lineTo(Q,q),t.lineTo(Q+W,q)}),t.stroke()}t.globalAlpha=1;const ht=r=>o+(r-X)/Z*l;t.font="10px system-ui, sans-serif";for(const[r,L]of[[O,"LSL"],[nt,"USL"]]){if(r===void 0)continue;const E=ht(r);t.strokeStyle=d.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(E,S),t.lineTo(E,T),t.stroke(),t.setLineDash([]),t.fillStyle=d.warnBorder,t.textAlign="center",t.textBaseline="bottom",t.fillText(L,E,S-2)}t.font="11px system-ui, sans-serif",t.strokeStyle=d.border,t.lineWidth=1,t.beginPath(),t.moveTo(o,T),t.lineTo(o+l,T),t.stroke(),t.fillStyle=d.textMuted,t.textAlign="center",t.textBaseline="top";const C=Math.max(2,Math.floor(l/44)),k=Math.max(1,Math.ceil(x.length/C));for(let r=0;r<=x.length;r+=k){const L=r<x.length?x[r].rangeLow:x[x.length-1].rangeHigh,E=o+r*W;t.beginPath(),t.moveTo(E,T),t.lineTo(E,T+4),t.stroke(),t.fillText(Y.tick(L),E,T+6)}Y.unitLabel&&(t.save(),t.font="10px system-ui, sans-serif",t.fillStyle=d.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${Y.unitLabel})`,o+l,T+20),t.restore())}function e(n){const{plotX:c,plotMaxWidth:t,barWidth:d}=p;if(n<c||n>c+t)return-1;const o=Math.floor((n-c)/d);return o>=0&&o<x.length?o:-1}g.addEventListener("mousemove",n=>{const c=g.getBoundingClientRect(),t=e(n.clientX-c.left);if(t!==H&&(H=t,m()),t>=0){const d=x[t],o=i.map((l,S)=>`<span style="display:inline-block;width:9px;height:9px;border-radius:2px;background:${D(S)};margin-right:5px;"></span>${l.groupKey}: ${l.counts[t]}`).join("<br>");A.innerHTML=`<strong>${st(d.rangeLow,w,"engineering")} \u2013 ${st(d.rangeHigh,w,"engineering")}</strong><br>${o}`,A.style.display="block",bt(A,b,n.clientX,n.clientY)}else A.style.display="none"}),g.addEventListener("mouseleave",()=>{H!==-1&&(H=-1,m()),A.style.display="none"}),J=xt(b,()=>m()),m()}U();function wt(a){!F.some(w=>w.testNumber===a)||a===f||(f=a,K.value=String(a),U())}return{card:b,setTest:wt,destroy:()=>J?.disconnect()}}
@@ -1 +1 @@
1
- import{getColorScheme as ht}from"../../renderer/colorSchemes.js";import{categorical as gt}from"./palette.js";import{buildScatterData as bt,buildScatterDataGrouped as yt}from"../../stats/scatter.js";import{buildFacetTable as St}from"../../stats/facets.js";import{CLR as M}from"../toolbar.js";import{cardShell as Mt,observeResize as Tt,makeTooltip as wt,makeTestSelect as kt,makeWaferSelect as vt,chartFillHeight as Lt,applyCanvasFlow as Ct,drawAxisUnit as At,resolveChartCanvasColors as Bt,makeAxisFormat as nt}from"./chartShell.js";const s=52,Ot=16,i=16,Et=44;export function renderScatterPanel(j){const{title:ot="Test scatter",items:B,testDefs:_,colorScheme:it="default",onSaveImage:lt,groups:P}=j,{card:T,body:C,controlsRow:X}=Mt(ot,lt),R=_.filter(e=>e.testNumber!==void 0),b=!!(P&&P.length>0),Y=(P??[]).map(e=>e.key),at=new Map(Y.map((e,n)=>[e,n]));let m=j.xTestNumber??R[0]?.testNumber??null,f=j.yTestNumber??R[1]?.testNumber??m,O=null;const q=()=>O!==null?B[O]?[B[O]]:[]:B;function K(e,n,o){const c=document.createElement("label");Object.assign(c.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:M.label});const d=document.createElement("span");d.textContent=e;const p=kt(R,n,o,{maxWidth:"180px",emptyText:"No tests"});return c.append(d,p),{wrap:c,select:p}}const{wrap:st,select:rt}=K("X:",m,e=>{m=e,H()}),{wrap:ct,select:dt}=K("Y:",f,e=>{f=e,H()});X.append(st,ct),!b&&B.length>1&&X.appendChild(vt(B,O,e=>{O=e,H()}));const E=document.createElement("div");Object.assign(E.style,{color:M.warnText,background:M.warnBg,border:`1px solid ${M.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",marginBottom:"4px",display:"none"}),T.insertBefore(E,C);function J(){const e=b?[]:St(q(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);e.length>0?(E.textContent=`\u26A0 Mixed ${e.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.`,E.style.display=""):E.style.display="none"}J();const D=document.createElement("div");D.textContent=b?"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",Object.assign(D.style,{color:M.label,fontSize:"11px",marginBottom:"4px"}),T.insertBefore(D,C);const pt=ht(it),{forBin:mt}=pt,G=e=>b?e.group??"\u2014":String(e.hbin??0),Q=e=>b?gt(at.get(e)??0):mt(Number(e)),V=e=>b?e:e==="0"?"No bin data":`Bin ${e}`,g=new Set,A=document.createElement("div");Object.assign(A.style,{display:"flex",flexWrap:"wrap",gap:"4px",marginBottom:"6px"}),C.appendChild(A);const u=document.createElement("canvas");u.style.display="block",u.style.cursor="crosshair",C.appendChild(u);const F=window.devicePixelRatio||1,Ht=wt(T);let h=[],w=0,I=1,k=0,N=1;function $(e){const n=_.find(o=>o.testNumber===e);return{unit:n?.unit,limitLow:n?.limitLow,limitHigh:n?.limitHigh}}function xt(){const e=C.clientWidth,n=Math.max(200,Math.min(400,e*.65)),o=Lt(T,C,u,n);return{w:e,h:o,plotW:Math.max(10,e-s-Ot),plotH:Math.max(10,o-i-Et)}}function Z(){for(const e of A.querySelectorAll("[data-cat]")){const n=e.dataset.cat,o=g.size===0||g.has(n);e.style.opacity=o?"1":"0.35",e.style.boxShadow=g.has(n)?`0 0 0 2px ${M.text}`:"none"}}function tt(e){A.innerHTML="",g.clear();for(const n of e){const o=document.createElement("button");o.type="button",o.dataset.cat=n,o.title=`${V(n)} \u2014 click to filter`;const c=Q(n);Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:"4px",padding:"2px 7px",borderRadius:"10px",border:`1px solid ${M.menuBorder}`,background:"none",cursor:"pointer",fontSize:"11px",color:M.text,whiteSpace:"nowrap"});const d=document.createElement("span");Object.assign(d.style,{display:"inline-block",width:"9px",height:"9px",borderRadius:"50%",background:c,flexShrink:"0"}),o.append(d,document.createTextNode(V(n))),o.addEventListener("click",()=>{g.has(n)?g.delete(n):g.add(n),Z(),z()}),A.appendChild(o)}Z()}function z(){Ct(u,A.offsetHeight);const e=Bt(T),n=I-w,o=N-k,{w:c,h:d,plotW:p,plotH:r}=xt();u.width=Math.max(1,Math.floor(c*F)),u.height=Math.max(1,Math.floor(d*F)),u.style.width=`${c}px`,u.style.height=`${d}px`;const t=u.getContext("2d");if(t.setTransform(F,0,0,F,0,0),t.clearRect(0,0,c,d),h.length===0){t.font="12px system-ui, sans-serif",t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="middle",t.fillText("No data \u2014 select two parametric tests with values.",c/2,d/2);return}const y=4,v=nt(Math.max(Math.abs(w),Math.abs(I)),m!==null?$(m).unit:void 0),U=nt(Math.max(Math.abs(k),Math.abs(N)),f!==null?$(f).unit:void 0);t.font="10px system-ui, sans-serif",t.strokeStyle=e.border,t.lineWidth=.5,t.fillStyle=e.textMuted;for(let l=0;l<=y;l++){const S=w+n*l/y,a=s+l/y*p;t.beginPath(),t.moveTo(a,i),t.lineTo(a,i+r),t.stroke(),t.textAlign="center",t.textBaseline="top",t.fillText(v.tick(S),a,i+r+4);const L=k+o*l/y,x=i+(1-l/y)*r;t.beginPath(),t.moveTo(s,x),t.lineTo(s+p,x),t.stroke(),t.textAlign="right",t.textBaseline="middle",t.fillText(U.tick(L),s-4,x)}v.unitLabel&&At(t,v.unitLabel,s+p/2,i+r+24,e.textMuted),U.unitLabel&&(t.save(),t.font="10px system-ui, sans-serif",t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="middle",t.translate(6,i+r/2),t.rotate(-Math.PI/2),t.fillText(`(${U.unitLabel})`,0,0),t.restore());const W=g.size===0?h:h.filter(l=>g.has(G(l))),et=W.length>5e3?Math.ceil(W.length/5e3):1;t.globalAlpha=Math.max(.15,Math.min(.7,200/(W.length/et)));for(let l=0;l<W.length;l+=et){const S=W[l],a=s+(S.x-w)/n*p,L=i+(1-(S.y-k)/o)*r;t.beginPath(),t.arc(a,L,2.5,0,Math.PI*2),t.fillStyle=Q(G(S)),t.fill()}if(t.globalAlpha=1,m!==null&&f!==null){const l=$(m),S=$(f);t.strokeStyle=e.textMuted,t.fillStyle=e.textMuted,t.lineWidth=1,t.setLineDash([4,3]),t.font="9px system-ui, sans-serif",t.textBaseline="top";for(const[a,L]of[[l.limitLow,"LSL"],[l.limitHigh,"USL"]]){if(a===void 0||a<w||a>I)continue;const x=s+(a-w)/n*p;t.beginPath(),t.moveTo(x,i),t.lineTo(x,i+r),t.stroke(),t.textAlign="left",t.fillText(L,x+2,i+2)}for(const[a,L]of[[S.limitLow,"LSL"],[S.limitHigh,"USL"]]){if(a===void 0||a<k||a>N)continue;const x=i+(1-(a-k)/o)*r;t.beginPath(),t.moveTo(s,x),t.lineTo(s+p,x),t.stroke(),t.textAlign="left",t.fillText(L,s+3,x+2)}t.setLineDash([])}t.strokeStyle=e.border,t.lineWidth=1,t.beginPath(),t.moveTo(s,i),t.lineTo(s,i+r),t.moveTo(s,i+r),t.lineTo(s+p,i+r),t.stroke()}function H(){if(J(),R.length<2||m===null||f===null){h=[],tt([]),z();return}h=b?yt(P,m,f):bt(q(),m,f);let e;if(b){const n=new Set(h.map(G));e=Y.filter(o=>n.has(o))}else e=Array.from(new Set(h.map(n=>n.hbin??0))).sort((n,o)=>n-o).map(String);if(tt(e),h.length>0){const n=h.map(v=>v.x),o=h.map(v=>v.y),c=Math.min(...n),d=Math.max(...n),p=Math.min(...o),r=Math.max(...o),t=(d-c)*.05||1,y=(r-p)*.05||1;w=c-t,I=d+t,k=p-y,N=r+y}z()}const ft=Tt(T,()=>z());H();function ut(e,n){m=e,f=n,rt.value=String(e),dt.value=String(n),H()}return{card:T,setXY:ut,destroy:()=>ft.disconnect()}}
1
+ import{getColorScheme as ht}from"../../renderer/colorSchemes.js";import{categorical as gt}from"./palette.js";import{buildScatterData as bt,buildScatterDataGrouped as yt}from"../../stats/scatter.js";import{buildFacetTable as St}from"../../stats/facets.js";import{CLR as M}from"../toolbar.js";import{cardShell as Mt,observeResize as Tt,makeTooltip as wt,makeTestSelect as kt,makeWaferSelect as vt,chartFillHeight as Lt,applyCanvasFlow as Ct,drawAxisUnit as At,resolveChartCanvasColors as Bt,makeAxisFormat as nt}from"./chartShell.js";const r=52,Ot=16,l=16,Et=44;export function renderScatterPanel(j){const{title:ot="Test scatter",items:B,testDefs:_,colorScheme:it="default",onSaveImage:lt,groups:P}=j,{card:T,body:C,controlsRow:X}=Mt(ot,lt),R=_.filter(e=>e.testNumber!==void 0),b=!!(P&&P.length>0),Y=(P??[]).map(e=>e.key),at=new Map(Y.map((e,n)=>[e,n]));let m=j.xTestNumber??R[0]?.testNumber??null,f=j.yTestNumber??R[1]?.testNumber??m,O=null;const q=()=>O!==null?B[O]?[B[O]]:[]:B;function K(e,n,o){const i=document.createElement("label");Object.assign(i.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:M.label});const d=document.createElement("span");d.textContent=e;const p=kt(R,n,o,{maxWidth:"180px",emptyText:"No tests"});return i.append(d,p),{wrap:i,select:p}}const{wrap:st,select:rt}=K("X:",m,e=>{m=e,H()}),{wrap:ct,select:dt}=K("Y:",f,e=>{f=e,H()});X.append(st,ct),!b&&B.length>1&&X.appendChild(vt(B,O,e=>{O=e,H()}));const E=document.createElement("div");Object.assign(E.style,{color:M.warnText,background:M.warnBg,border:`1px solid ${M.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",marginBottom:"4px",display:"none"}),T.insertBefore(E,C);function J(){const e=b?[]:St(q(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);e.length>0?(E.textContent=`\u26A0 Mixed ${e.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.`,E.style.display=""):E.style.display="none"}J();const D=document.createElement("div");D.textContent=b?"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",Object.assign(D.style,{color:M.label,fontSize:"11px",marginBottom:"4px"}),T.insertBefore(D,C);const pt=ht(it),{forBin:mt}=pt,G=e=>b?e.group??"\u2014":String(e.hbin??0),Q=e=>b?gt(at.get(e)??0):mt(Number(e)),V=e=>b?e:e==="0"?"No bin data":`Bin ${e}`,g=new Set,A=document.createElement("div");Object.assign(A.style,{display:"flex",flexWrap:"wrap",gap:"4px",marginBottom:"6px"}),C.appendChild(A);const u=document.createElement("canvas");u.style.display="block",u.style.cursor="crosshair",C.appendChild(u);const F=window.devicePixelRatio||1,Ht=wt(T);let h=[],w=0,I=1,k=0,N=1;function $(e){const n=_.find(o=>o.testNumber===e);return{unit:n?.unit,limitLow:n?.limitLow,limitHigh:n?.limitHigh}}function xt(){const e=C.clientWidth,n=Math.max(200,Math.min(400,e*.65)),o=Lt(T,C,u,n);return{w:e,h:o,plotW:Math.max(10,e-r-Ot),plotH:Math.max(10,o-l-Et)}}function Z(){for(const e of A.querySelectorAll("[data-cat]")){const n=e.dataset.cat,o=g.size===0||g.has(n);e.style.opacity=o?"1":"0.35";const i=g.has(n);e.style.boxShadow=i?`0 0 0 2px ${M.text}`:"none",e.setAttribute("aria-pressed",i?"true":"false")}}function tt(e){A.innerHTML="",g.clear();for(const n of e){const o=document.createElement("button");o.type="button",o.dataset.cat=n,o.title=`${V(n)} \u2014 click to filter`;const i=Q(n);Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:"4px",padding:"2px 7px",borderRadius:"10px",border:`1px solid ${M.menuBorder}`,background:"none",cursor:"pointer",fontSize:"11px",color:M.text,whiteSpace:"nowrap"});const d=document.createElement("span");Object.assign(d.style,{display:"inline-block",width:"9px",height:"9px",borderRadius:"50%",background:i,flexShrink:"0"}),o.append(d,document.createTextNode(V(n))),o.addEventListener("click",()=>{g.has(n)?g.delete(n):g.add(n),Z(),z()}),A.appendChild(o)}Z()}function z(){Ct(u,A.offsetHeight);const e=Bt(T),n=I-w,o=N-k,{w:i,h:d,plotW:p,plotH:c}=xt();u.width=Math.max(1,Math.floor(i*F)),u.height=Math.max(1,Math.floor(d*F)),u.style.width=`${i}px`,u.style.height=`${d}px`;const t=u.getContext("2d");if(t.setTransform(F,0,0,F,0,0),t.clearRect(0,0,i,d),h.length===0){t.font="12px system-ui, sans-serif",t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="middle",t.fillText("No data \u2014 select two parametric tests with values.",i/2,d/2);return}const y=4,v=nt(Math.max(Math.abs(w),Math.abs(I)),m!==null?$(m).unit:void 0),U=nt(Math.max(Math.abs(k),Math.abs(N)),f!==null?$(f).unit:void 0);t.font="10px system-ui, sans-serif",t.strokeStyle=e.border,t.lineWidth=.5,t.fillStyle=e.textMuted;for(let a=0;a<=y;a++){const S=w+n*a/y,s=r+a/y*p;t.beginPath(),t.moveTo(s,l),t.lineTo(s,l+c),t.stroke(),t.textAlign="center",t.textBaseline="top",t.fillText(v.tick(S),s,l+c+4);const L=k+o*a/y,x=l+(1-a/y)*c;t.beginPath(),t.moveTo(r,x),t.lineTo(r+p,x),t.stroke(),t.textAlign="right",t.textBaseline="middle",t.fillText(U.tick(L),r-4,x)}v.unitLabel&&At(t,v.unitLabel,r+p/2,l+c+24,e.textMuted),U.unitLabel&&(t.save(),t.font="10px system-ui, sans-serif",t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="middle",t.translate(6,l+c/2),t.rotate(-Math.PI/2),t.fillText(`(${U.unitLabel})`,0,0),t.restore());const W=g.size===0?h:h.filter(a=>g.has(G(a))),et=W.length>5e3?Math.ceil(W.length/5e3):1;t.globalAlpha=Math.max(.15,Math.min(.7,200/(W.length/et)));for(let a=0;a<W.length;a+=et){const S=W[a],s=r+(S.x-w)/n*p,L=l+(1-(S.y-k)/o)*c;t.beginPath(),t.arc(s,L,2.5,0,Math.PI*2),t.fillStyle=Q(G(S)),t.fill()}if(t.globalAlpha=1,m!==null&&f!==null){const a=$(m),S=$(f);t.strokeStyle=e.textMuted,t.fillStyle=e.textMuted,t.lineWidth=1,t.setLineDash([4,3]),t.font="9px system-ui, sans-serif",t.textBaseline="top";for(const[s,L]of[[a.limitLow,"LSL"],[a.limitHigh,"USL"]]){if(s===void 0||s<w||s>I)continue;const x=r+(s-w)/n*p;t.beginPath(),t.moveTo(x,l),t.lineTo(x,l+c),t.stroke(),t.textAlign="left",t.fillText(L,x+2,l+2)}for(const[s,L]of[[S.limitLow,"LSL"],[S.limitHigh,"USL"]]){if(s===void 0||s<k||s>N)continue;const x=l+(1-(s-k)/o)*c;t.beginPath(),t.moveTo(r,x),t.lineTo(r+p,x),t.stroke(),t.textAlign="left",t.fillText(L,r+3,x+2)}t.setLineDash([])}t.strokeStyle=e.border,t.lineWidth=1,t.beginPath(),t.moveTo(r,l),t.lineTo(r,l+c),t.moveTo(r,l+c),t.lineTo(r+p,l+c),t.stroke()}function H(){if(J(),R.length<2||m===null||f===null){h=[],tt([]),z();return}h=b?yt(P,m,f):bt(q(),m,f);let e;if(b){const n=new Set(h.map(G));e=Y.filter(o=>n.has(o))}else e=Array.from(new Set(h.map(n=>n.hbin??0))).sort((n,o)=>n-o).map(String);if(tt(e),h.length>0){const n=h.map(v=>v.x),o=h.map(v=>v.y),i=Math.min(...n),d=Math.max(...n),p=Math.min(...o),c=Math.max(...o),t=(d-i)*.05||1,y=(c-p)*.05||1;w=i-t,I=d+t,k=p-y,N=c+y}z()}const ft=Tt(T,()=>z());H();function ut(e,n){m=e,f=n,rt.value=String(e),dt.value=String(n),H()}return{card:T,setXY:ut,destroy:()=>ft.disconnect()}}
@@ -0,0 +1,41 @@
1
+ import type { Die } from '../core/dies.js';
2
+ import { type TestDef } from '../renderer/buildWaferMap.js';
3
+ import { type SaveTextHandler } from './toolbar.js';
4
+ export interface DieListOptions {
5
+ /** Heading text. Default: `Die list (N)`. */
6
+ title?: string;
7
+ /** Optional note shown under the heading, e.g. explaining why this replaced the map. */
8
+ note?: string;
9
+ /** CSV filename, including extension. Default: `'dies.csv'`. */
10
+ csvFilename?: string;
11
+ /** Host hook for "Export CSV" — see `saveTextFile` (toolbar.ts). */
12
+ onSaveText?: SaveTextHandler;
13
+ /**
14
+ * Extra leading column, e.g. a wafer-id label for a lot-level combined
15
+ * list spanning multiple wafers. `get(die)` returning `undefined` renders
16
+ * an empty cell for that row.
17
+ */
18
+ extraColumn?: {
19
+ label: string;
20
+ get: (die: Die) => string | undefined;
21
+ };
22
+ /**
23
+ * Cap the scrolling table at this CSS length instead of letting it fill.
24
+ *
25
+ * Default (omitted) is **fill**: the table grows to consume whatever height
26
+ * its container gives it (`flex: 1; min-height: 0`), which is what every
27
+ * in-library caller wants — a card overlay, a footer panel and a resizable
28
+ * modal all have a definite height, and a fixed cap left a short table
29
+ * floating in a half-empty box. In a container with NO definite height the
30
+ * table simply renders at its natural full length and the host page
31
+ * scrolls; set this if that isn't wanted (e.g. mounting a 10k-row list
32
+ * directly into a long document).
33
+ */
34
+ maxHeight?: string;
35
+ }
36
+ /**
37
+ * Build a scrollable die-list table + "Export CSV" button. Returns `null`
38
+ * when `dies` is empty (nothing to show).
39
+ */
40
+ export declare function buildDieListSection(dies: Die[], testDefs: TestDef[] | undefined, options?: DieListOptions): HTMLDivElement | null;
41
+ //# sourceMappingURL=dieList.d.ts.map
@@ -0,0 +1,2 @@
1
+ import{hasPosition as S}from"../core/dies.js";import{isParametricTest as v}from"../renderer/buildWaferMap.js";import{CLR as r,saveTextFile as w}from"./toolbar.js";import{csvField as C}from"./summaryPanel.js";import{fmt as y}from"../renderer/fmt.js";function o(n,d,t){const l=document.createElement(n);return d&&Object.assign(l.style,d),t!==void 0&&(l.textContent=t),l}function B(n){return S(n)?`(${n.x}, ${n.y})`:"\u2014"}function $(n,d){if(d?.length)return d.filter(v);const t=new Map;for(const l of n)for(const a of Object.keys(l.testValues??{})){const s=Number(a);t.has(s)||t.set(s,{testNumber:s,name:`Test ${s}`})}return[...t.values()].sort((l,a)=>l.testNumber-a.testNumber)}export function buildDieListSection(n,d,t={}){if(!n.length)return null;const l=$(n,d),a=o("div",{display:"flex",flexDirection:"column",gap:"8px",flex:"1",minHeight:"0"}),s=o("div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});s.appendChild(o("div",{fontSize:"10px",fontWeight:"700",letterSpacing:"0.06em",textTransform:"uppercase",color:r.label},t.title??`Die list (${n.length})`));const f=o("button",{fontSize:"11px",padding:"3px 8px",borderRadius:"4px",border:`1px solid ${r.menuBorder}`,background:r.menuBg,color:r.text,cursor:"pointer"},"Export CSV");f.type="button",s.appendChild(f),a.appendChild(s),t.note&&a.appendChild(o("div",{fontSize:"11px",color:r.label,lineHeight:"1.4",flexShrink:"0"},t.note));const m=o("div",{overflow:"auto",flex:"1",minHeight:"0",border:`1px solid ${r.menuBorder}`,borderRadius:"4px",...t.maxHeight?{maxHeight:t.maxHeight}:{}}),c=o("table",{width:"100%",borderCollapse:"collapse",fontSize:"11px"});c.setAttribute("aria-label",t.title??`Die list (${n.length} dies)`);const u=[...t.extraColumn?[{label:t.extraColumn.label,get:e=>t.extraColumn.get(e)??""}]:[],{label:"Position",get:B},{label:"Site",get:e=>e.siteNum!==void 0?String(e.siteNum):""},{label:"Hard bin",get:e=>e.hbin!==void 0?String(e.hbin):""},{label:"Soft bin",get:e=>e.sbin!==void 0?String(e.sbin):""},...l.map(e=>({label:e.name,get:i=>{const p=i.testValues?.[e.testNumber];if(p!==void 0)return y(p,e.unit);const g=i.testPass?.[e.testNumber];return g===void 0?"":g?"PASS":"FAIL"}}))],b=o("thead"),x=o("tr");for(const e of u){const i=o("th",{textAlign:"left",padding:"4px 8px",position:"sticky",top:"0",background:r.panelBg,color:r.label,fontWeight:"600",borderBottom:`1px solid ${r.menuBorder}`,whiteSpace:"nowrap"},e.label);i.scope="col",x.appendChild(i)}b.appendChild(x),c.appendChild(b);const h=o("tbody");for(const e of n){const i=o("tr");for(const p of u)i.appendChild(o("td",{padding:"3px 8px",color:r.text,borderBottom:`1px solid ${r.menuBorder}`,whiteSpace:"nowrap"},p.get(e)));h.appendChild(i)}return c.appendChild(h),m.appendChild(c),a.appendChild(m),f.addEventListener("click",()=>{const e=[u.map(i=>C(i.label)).join(",")];for(const i of n)e.push(u.map(p=>C(p.get(i))).join(","));w(e.join(`
2
+ `),t.csvFilename??"dies.csv","text/csv",t.onSaveText)}),a}
@@ -5,6 +5,8 @@ export type { RenderOptions, WaferMapController, WaferViewOptions, WaferPreferen
5
5
  export { renderWaferGallery } from './renderWaferGallery.js';
6
6
  export type { WaferMapDisplayItem, WaferMapDisplayItemFactory, GalleryOptions, GalleryController } from './renderWaferGallery.js';
7
7
  export type { SummaryPanelOptions } from './summaryPanel.js';
8
+ export { buildDieListSection } from './dieList.js';
9
+ export type { DieListOptions } from './dieList.js';
8
10
  export { collectWarnings, severityOf } from './warnings.js';
9
11
  export type { WarningsOptions } from './warnings.js';
10
12
  export type { InsightsOptions, InsightsView } from './insightsTab.js';
@@ -1 +1 @@
1
- export{toCanvas}from"./toCanvas.js";export{renderWaferMap}from"./renderWaferMap.js";export{renderWaferGallery}from"./renderWaferGallery.js";export{collectWarnings,severityOf}from"./warnings.js";export{setDetachWindowOpener}from"./toolbar.js";
1
+ export{toCanvas}from"./toCanvas.js";export{renderWaferMap}from"./renderWaferMap.js";export{renderWaferGallery}from"./renderWaferGallery.js";export{buildDieListSection}from"./dieList.js";export{collectWarnings,severityOf}from"./warnings.js";export{setDetachWindowOpener}from"./toolbar.js";
@@ -0,0 +1,34 @@
1
+ import type { Die } from '../core/dies.js';
2
+ import type { TestDef, BinDef } from '../renderer/buildWaferMap.js';
3
+ import type { PlotMode } from '../renderer/buildView.js';
4
+ export interface MaplessSummaryOptions {
5
+ plotMode: PlotMode;
6
+ activeTest?: number;
7
+ hbinDefs?: BinDef[];
8
+ sbinDefs?: BinDef[];
9
+ colorScheme?: string;
10
+ /** Effective log-scale setting for the active test — same resolution the
11
+ * map itself uses (explicit viewOpts.logScale, falling back to the
12
+ * TestDef's own default). */
13
+ logScale?: boolean;
14
+ /** 'spec' colours the full spec-limit window (like the map's colorbar
15
+ * range toggle); 'data' colours just the observed value range. Default 'spec'. */
16
+ colorbarRangeMode?: 'data' | 'spec';
17
+ /**
18
+ * Full wafer/lot die population for this test (positioned + unpositioned),
19
+ * used only to compute the value→colour range — NOT what gets bucketed
20
+ * into bars, which always stays scoped to `dies` (the caller's own
21
+ * argument, below). Falls back to `dies` when omitted. Passing the full
22
+ * population (e.g. a mixed wafer's footer passing every die, not just the
23
+ * unpositioned subset it's charting) keeps bar colours consistent with
24
+ * whatever range the wafer's own visible map is using for the same test.
25
+ */
26
+ valueRangeDies?: Die[];
27
+ }
28
+ /**
29
+ * Compact, plot-mode-aware summary for dies with no position data. Always
30
+ * returns a mountable element — an unhandled mode/empty data still renders a
31
+ * one-line message rather than leaving the caller with nothing to show.
32
+ */
33
+ export declare function buildMaplessSummary(dies: Die[], testDefs: TestDef[] | undefined, options: MaplessSummaryOptions): HTMLElement;
34
+ //# sourceMappingURL=maplessSummary.d.ts.map
@@ -0,0 +1 @@
1
+ import{getDieTestValue as R}from"../renderer/buildWaferMap.js";import{resolveTestNumber as T,findTestDef as $}from"../renderer/buildView.js";import{buildBinSection as y}from"./summaryPanel.js";import{buildTestHistogramData as B}from"../stats/histogram.js";import{getColorScheme as z}from"../renderer/colorSchemes.js";import{CLR as u,getTooltip as E,positionTooltip as H,hideTooltip as V}from"./toolbar.js";import{fmt as b}from"../renderer/fmt.js";function c(i,n,e){const t=document.createElement(i);return n&&Object.assign(t.style,n),e!==void 0&&(t.textContent=e),t}function F(i,n){i.tabIndex=0,i.setAttribute("role","img"),i.setAttribute("aria-label",n);const e=m=>{const l=E(i.ownerDocument);if(l.textContent=n,l.style.display="block",m)H(l,i,m.clientX,m.clientY);else{const r=i.getBoundingClientRect();H(l,i,r.left+r.width/2,r.top)}},t=()=>V(i.ownerDocument);i.addEventListener("mousemove",e),i.addEventListener("mouseleave",t),i.addEventListener("focus",()=>e()),i.addEventListener("blur",t)}function I(i,n,e,t,m){let l=1/0,r=-1/0;for(const p of i){const a=R(p,n);a!==void 0&&(a<l&&(l=a),a>r&&(r=a))}const f=t==="spec"&&e&&(e.limitLow!==void 0||e.limitHigh!==void 0);let s,d;f?(s=e.limitLow!==void 0?e.limitLow:isFinite(l)?l:0,d=e.limitHigh!==void 0?e.limitHigh:isFinite(r)?r:1):(s=isFinite(l)?l:0,d=isFinite(r)?r:1),s===d&&(d=s+1);const x=p=>Math.min(1,Math.max(0,p));if(m&&s>0&&d>0){const p=Math.log10(s),a=Math.log10(d)-p;return h=>h<=0?0:x((Math.log10(h)-p)/a)}return p=>x((p-s)/(d-s))}function N(i,n,e,t,m,l,r){const f=c("div",{display:"flex",flexDirection:"column",gap:"2px",minWidth:"0",flex:"1",minHeight:"0"});f.appendChild(c("div",{fontSize:"10px",fontWeight:"700",letterSpacing:"0.06em",textTransform:"uppercase",color:u.label},n?.name?`${n.name} distribution`:"Value distribution"));const s=i[0].rangeLow,d=i[i.length-1].rangeHigh,x=d-s||1,p=o=>Math.min(1,Math.max(0,(o-s)/x)),a=z(t),h=I(m,e,n,l,r),S=Math.max(1,...i.map(o=>o.count)),L=c("div",{position:"relative",display:"flex",alignItems:"flex-end",gap:"2px",flex:"1",minHeight:"48px"});for(const o of i){const g=c("div",{flex:"1",minWidth:"2px",background:a.forValue(h((o.rangeLow+o.rangeHigh)/2)),height:`${o.count/S*100}%`,borderRadius:"1px 1px 0 0"});g.dataset.wmapBar="1",F(g,`${b(o.rangeLow,n?.unit)} \u2013 ${b(o.rangeHigh,n?.unit)}: ${o.count}`),L.appendChild(g)}const M=[];for(const[o,g]of[[n?.limitLow,"LSL"],[n?.limitHigh,"USL"]]){if(o===void 0||o<s||o>d)continue;const v=p(o)*100,w=c("div",{position:"absolute",top:"0",bottom:"0",left:`${v}%`,width:"2px",marginLeft:"-1px",background:u.value,boxShadow:`0 0 0 1px ${u.panelBg}`,pointerEvents:"none"});w.dataset.wmapLimitMarker="1",L.appendChild(w),M.push({pct:v,text:`${g} ${b(o,n?.unit)}`})}if(f.appendChild(L),M.length){const o=c("div",{position:"relative",height:"14px"});for(const{pct:g,text:v}of M){const w=Math.min(96,Math.max(4,g));o.appendChild(c("span",{position:"absolute",left:`${w}%`,top:"0",transform:"translateX(-50%)",whiteSpace:"nowrap",fontSize:"9px",fontWeight:"600",color:u.text,background:u.panelBg,border:`1px solid ${u.value}`,borderRadius:"2px",padding:"0 3px",lineHeight:"12px"},v))}f.appendChild(o)}const C=c("div",{display:"flex",justifyContent:"space-between",fontSize:"10px",color:u.label});return C.appendChild(c("span",{},b(i[0].rangeLow,n?.unit))),C.appendChild(c("span",{},b(i[i.length-1].rangeHigh,n?.unit))),f.appendChild(C),f}export function buildMaplessSummary(i,n,e){const{plotMode:t,activeTest:m,hbinDefs:l,sbinDefs:r,colorScheme:f,logScale:s=!1,colorbarRangeMode:d="spec",valueRangeDies:x}=e;if(t==="hardBin"||t==="softBin"){const a=y(i,t==="hardBin"?l:r,t==="hardBin"?"hard":"soft",f);if(a)return a}else if(t==="value"){const{testNumber:p}=T(m??0,n),a=$(n,m??0),h=B([{dies:i}],p,12,a?.limitLow,a?.limitHigh);if(h.length)return N(h,a,p,f,x??i,d,s)}return c("div",{padding:"12px",fontSize:"11px",color:u.label,textAlign:"center"},"No summary available for this view.")}
@@ -1,6 +1,7 @@
1
1
  import { type SaveImageHandler, type SaveTextHandler, type UserGuideExtension } from './toolbar.js';
2
2
  import type { Die } from '../core/dies.js';
3
3
  import type { WaferViewOptions } from './renderWaferMap.js';
4
+ import type { RenderableWaferMap } from './renderWaferMap.js';
4
5
  import type { LotStatsSummary } from '../stats/types.js';
5
6
  import { type WarningsOptions } from './warnings.js';
6
7
  import type { SummaryPanelOptions } from './summaryPanel.js';
@@ -18,24 +19,7 @@ import { type InsightsOptions } from './insightsTab.js';
18
19
  * ]);
19
20
  * ```
20
21
  */
21
- export interface WaferMapDisplayItem {
22
- wafer: import('../core/wafer.js').Wafer;
23
- dies: Die[];
24
- hbinDefs?: import('../renderer/buildWaferMap.js').BinDef[];
25
- sbinDefs?: import('../renderer/buildWaferMap.js').BinDef[];
26
- testDefs?: import('../renderer/buildWaferMap.js').TestDef[];
27
- metadataFields?: import('../renderer/buildWaferMap.js').MetadataFieldDef[];
28
- reticles?: import('../core/reticle.js').Reticle[];
29
- /**
30
- * Geometry advisories carried from `buildWaferMap`. Present automatically when
31
- * an item is spread from a `WaferMapResult` (the usual `{ ...result, label }`
32
- * shape); the gallery collects them across cards and surfaces them once in the
33
- * lot bar's warning indicator.
34
- */
35
- warnings?: import('../renderer/buildWaferMap.js').WaferWarning[];
36
- isLotStack?: boolean;
37
- aggrMethod?: string;
38
- lotSize?: number;
22
+ export interface WaferMapDisplayItem extends RenderableWaferMap {
39
23
  label?: string;
40
24
  /** Merged on top of the shared gallery options for this card only. */
41
25
  viewOptions?: Partial<WaferViewOptions>;