@wafertools/wafermap 0.26.1 → 0.28.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.
- package/AGENTS.md +10 -2
- package/CHANGELOG.md +670 -0
- package/README.md +46 -3
- package/dist/packages/canvas-adapter/canvasTheme.d.ts +4 -0
- package/dist/packages/canvas-adapter/canvasTheme.js +1 -1
- package/dist/packages/canvas-adapter/charts/barPanel.d.ts +16 -0
- package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
- package/dist/packages/canvas-adapter/charts/binCluster.d.ts +0 -1
- package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
- package/dist/packages/canvas-adapter/charts/boxplot.d.ts +21 -2
- package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
- package/dist/packages/canvas-adapter/charts/capability.d.ts +10 -11
- package/dist/packages/canvas-adapter/charts/capability.js +1 -1
- package/dist/packages/canvas-adapter/charts/chartShell.d.ts +337 -3
- package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
- package/dist/packages/canvas-adapter/charts/correlation.d.ts +8 -10
- package/dist/packages/canvas-adapter/charts/correlation.js +2 -1
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +50 -0
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -0
- package/dist/packages/canvas-adapter/charts/histogram.d.ts +12 -2
- package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +0 -1
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
- package/dist/packages/canvas-adapter/charts/scatter.d.ts +6 -1
- package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
- package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +20 -0
- package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -0
- package/dist/packages/canvas-adapter/charts/trend.d.ts +37 -0
- package/dist/packages/canvas-adapter/charts/trend.js +1 -0
- package/dist/packages/canvas-adapter/dieList.js +7 -7
- package/dist/packages/canvas-adapter/hitGrid.d.ts +22 -0
- package/dist/packages/canvas-adapter/hitGrid.js +1 -0
- package/dist/packages/canvas-adapter/identityHeader.d.ts +46 -0
- package/dist/packages/canvas-adapter/identityHeader.js +1 -0
- package/dist/packages/canvas-adapter/index.d.ts +2 -1
- package/dist/packages/canvas-adapter/index.js +1 -1
- package/dist/packages/canvas-adapter/insightsTab.d.ts +51 -5
- package/dist/packages/canvas-adapter/insightsTab.js +1 -1
- package/dist/packages/canvas-adapter/maplessSummary.d.ts +5 -1
- package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +15 -2
- package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -20
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +110 -18
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +233 -37
- package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
- package/dist/packages/canvas-adapter/toCanvas.d.ts +6 -0
- package/dist/packages/canvas-adapter/toCanvas.js +1 -1
- package/dist/packages/canvas-adapter/toolbar.d.ts +352 -15
- package/dist/packages/canvas-adapter/toolbar.js +2 -2
- package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
- package/dist/packages/canvas-adapter/userGuideHtml.js +115 -41
- package/dist/packages/canvas-adapter/version.d.ts +2 -2
- package/dist/packages/canvas-adapter/version.js +1 -1
- package/dist/packages/canvas-adapter/warnings.d.ts +21 -0
- package/dist/packages/canvas-adapter/warnings.js +1 -1
- package/dist/packages/core/dies.d.ts +11 -0
- package/dist/packages/core/dies.js +1 -1
- package/dist/packages/core/utils.d.ts +11 -0
- package/dist/packages/core/utils.js +1 -1
- package/dist/packages/renderer/binColors.d.ts +80 -0
- package/dist/packages/renderer/binColors.js +1 -0
- package/dist/packages/renderer/buildView.d.ts +100 -6
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.d.ts +112 -7
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/renderer/colorMap.d.ts +16 -15
- package/dist/packages/renderer/colorMap.js +1 -1
- package/dist/packages/renderer/colorSchemes.d.ts +45 -26
- package/dist/packages/renderer/colorSchemes.js +1 -1
- package/dist/packages/renderer/index.d.ts +3 -1
- package/dist/packages/renderer/index.js +1 -1
- package/dist/packages/stats/analyzeWaferLot.js +1 -1
- package/dist/packages/stats/analyzeWaferMap.js +1 -1
- package/dist/packages/stats/binPareto.d.ts +15 -0
- package/dist/packages/stats/binPareto.js +1 -1
- package/dist/packages/stats/clusterDetection.js +1 -1
- package/dist/packages/stats/connectedComponents.d.ts +14 -0
- package/dist/packages/stats/connectedComponents.js +1 -0
- package/dist/packages/stats/correlation.d.ts +26 -0
- package/dist/packages/stats/correlation.js +1 -1
- package/dist/packages/stats/facets.js +1 -1
- package/dist/packages/stats/filterFindings.d.ts +18 -0
- package/dist/packages/stats/filterFindings.js +1 -1
- package/dist/packages/stats/findingsNarrative.js +1 -1
- package/dist/packages/stats/histogram.d.ts +47 -2
- package/dist/packages/stats/histogram.js +1 -1
- package/dist/packages/stats/index.d.ts +6 -0
- package/dist/packages/stats/index.js +1 -1
- package/dist/packages/stats/mergeTestDefs.d.ts +44 -0
- package/dist/packages/stats/mergeTestDefs.js +1 -0
- package/dist/packages/stats/metadataColumns.d.ts +20 -4
- package/dist/packages/stats/metadataColumns.js +1 -1
- package/dist/packages/stats/patternClassification.js +1 -1
- package/dist/packages/stats/population.d.ts +23 -0
- package/dist/packages/stats/population.js +1 -0
- package/dist/packages/stats/renderFindingsReport.js +15 -15
- package/dist/packages/stats/renderSummaryReport.js +35 -32
- package/dist/packages/stats/reportHtml.js +38 -13
- package/dist/packages/stats/testPassRate.d.ts +85 -0
- package/dist/packages/stats/testPassRate.js +1 -0
- package/dist/packages/stats/trend.d.ts +40 -0
- package/dist/packages/stats/trend.js +1 -0
- package/dist/packages/stats/types.d.ts +117 -6
- package/dist/packages/stats/yield.js +1 -1
- package/llms.txt +6 -0
- package/package.json +9 -5
- package/dist/packages/canvas-adapter/metadataBadge.d.ts +0 -30
- package/dist/packages/canvas-adapter/metadataBadge.js +0 -1
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { type HistogramItem } from '../../stats/histogram.js';
|
|
2
2
|
import type { TestDef } from '../../renderer/buildWaferMap.js';
|
|
3
|
-
import { type SaveImageHandler } from './chartShell.js';
|
|
3
|
+
import { type SaveImageHandler, type AxisPrefs } from './chartShell.js';
|
|
4
4
|
export interface HistogramPanelOptions {
|
|
5
|
+
/** Initial axis toggles, shared with the sibling distribution panels. */
|
|
6
|
+
axisPrefs?: AxisPrefs;
|
|
7
|
+
/** Fired when the user changes an axis toggle here, so siblings can follow. */
|
|
8
|
+
onAxisPrefsChange?: (prefs: AxisPrefs) => void;
|
|
9
|
+
/** Fired when the USER picks a test here, so siblings can follow. */
|
|
10
|
+
onTestChange?: (testNumber: number) => void;
|
|
5
11
|
title?: string;
|
|
6
12
|
items: HistogramItem[];
|
|
7
13
|
testDefs: TestDef[];
|
|
8
14
|
selectedTestNumber?: number;
|
|
9
|
-
colorScheme?: string;
|
|
10
15
|
onSaveImage?: SaveImageHandler;
|
|
11
16
|
/**
|
|
12
17
|
* When the Analysis tab's "Group by" is active, this panel switches to an
|
|
@@ -19,6 +24,9 @@ export interface HistogramPanelOptions {
|
|
|
19
24
|
key: string;
|
|
20
25
|
items: HistogramItem[];
|
|
21
26
|
}[];
|
|
27
|
+
/** Fired when the user narrows to one group by clicking it in the legend.
|
|
28
|
+
* The Insights tab owns the scope; this panel only reports the gesture. */
|
|
29
|
+
onGroupChange?: (key: string | null) => void;
|
|
22
30
|
/** Document to build this panel's DOM into. Default `document` — pass the
|
|
23
31
|
* host's own `ownerDocument` when the container might live in a
|
|
24
32
|
* different document (e.g. a gallery card detached into its own popup
|
|
@@ -29,6 +37,8 @@ export interface HistogramPanelHandle {
|
|
|
29
37
|
card: HTMLElement;
|
|
30
38
|
/** Cross-panel link (e.g. from the capability panel): switch to `testNumber` in place. */
|
|
31
39
|
setTest: (testNumber: number) => void;
|
|
40
|
+
/** Adopt the shared axis toggles (see `AxisPrefs`). */
|
|
41
|
+
setAxisPrefs: (prefs: AxisPrefs) => void;
|
|
32
42
|
destroy: () => void;
|
|
33
43
|
}
|
|
34
44
|
export declare function renderHistogramPanel(options: HistogramPanelOptions): HistogramPanelHandle;
|
|
@@ -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 Ct,categorical as Wt}from"./palette.js";import{cardShell as Dt,observeResize as xt,makeTooltip as Et,attachChartTip 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 wt,PADDING as tt}from"./chartShell.js";const mt=230,vt=36,at=18;function St(l,B,N,z,rt,R,i,f=4){const j=Math.max(1,R/f),F=Math.pow(10,Math.floor(Math.log10(j))),x=j/F,G=(x<=1?1:x<=2?2:x<=5?5:10)*F,P=z-N;l.save(),l.font="10px system-ui, sans-serif",l.textAlign="right",l.textBaseline="middle";for(let I=0;I<=R+1e-9;I+=G){const _=z-I/(R||1)*P;l.strokeStyle=i.grid,l.lineWidth=1,l.beginPath(),l.moveTo(B,_),l.lineTo(B+rt,_),l.stroke(),l.fillStyle=i.text,l.fillText(String(Math.round(I)),B-4,_)}l.strokeStyle=i.axis,l.lineWidth=1,l.beginPath(),l.moveTo(B,N),l.lineTo(B,z),l.stroke(),l.restore()}export function renderHistogramPanel(l){const{title:B="Test value distribution",items:N,testDefs:z,onSaveImage:rt,groups:R}=l,{card:i,body:f,controlsRow:j}=Dt(B,rt,l.ownerDocument),F=z.filter(r=>r.testNumber!==void 0);let x=l.selectedTestNumber??F[0]?.testNumber??null,G=null,P=!1;const I=$t(F,x,r=>{x=r,U()},{maxWidth:"200px",ownerDocument:i.ownerDocument});j.appendChild(I);const _=Bt(N,G,r=>{G=r,U()},{ownerDocument:i.ownerDocument});j.appendChild(_),j.appendChild(Rt("Axis includes limits",P,r=>{P=r,U()},i.ownerDocument));const L=Et(i);let J=null,y=null;function et(r){const H=z.find(K=>K.testNumber===r);return{unit:H?.unit,limitLow:H?.limitLow,limitHigh:H?.limitHigh}}function U(){if(J?.disconnect(),J=null,f.innerHTML="",x===null){pt(f,"No parametric test data available for a histogram.");return}const r=R&&R.length>0?At(R,x,16,P?et(x).limitLow:void 0,P?et(x).limitHigh:void 0):null;if(_.style.display=r?"none":"",r){if(r.series.length===0){pt(f,"No parametric test data available for a histogram.");return}kt(r);return}const H=G!==null?[N[G]]:N,{unit:K,limitLow:nt,limitHigh:b}=et(x),s=Ht(H,x,16,P?nt:void 0,P?b:void 0);if(s.length===0){pt(f,"No parametric test data available for a histogram.");return}const O=Math.max(...s.map(m=>m.count),1),E=i.ownerDocument.createElement("div");Object.assign(E.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),E.textContent=`max ${O} dies/bucket`,f.appendChild(E);const g=i.ownerDocument.createElement("canvas");g.style.display="block",g.style.cursor="default",f.appendChild(g);let w=-1;const p=window.devicePixelRatio||1,$=s[0].rangeLow,X=s[s.length-1].rangeHigh,ot=X-$||1,Z=wt(Math.max(Math.abs($),Math.abs(X)),K);function Y(m){const u=tt+36,e=g.clientWidth-u-tt,n=m-vt-at;return{plotX:u,plotMaxWidth:Math.max(10,e),plotMaxHeight:n,plotTop:at}}function ct(m){const{plotX:u,plotMaxWidth:e}=Y(mt);if(m<u||m>u+e)return-1;const n=Math.floor((m-u)/e*s.length);return n>=0&&n<s.length?n:-1}function A(){Mt(g,E.offsetHeight);const m=f.clientWidth,u=yt(i,f,g,mt);g.width=Math.max(1,Math.floor(m*p)),g.height=Math.max(1,Math.floor(u*p)),g.style.width=`${m}px`,g.style.height=`${u}px`;const e=g.getContext("2d");e.setTransform(p,0,0,p,0,0),e.clearRect(0,0,m,u),e.font="11px system-ui, sans-serif";const n=Tt(i),{plotX:d,plotMaxWidth:t,plotMaxHeight:h,plotTop:o}=Y(u),a=o+h,v=t/s.length;St(e,d,o,a,t,O,{text:n.textMuted,axis:n.border,grid:n.border}),s.forEach((M,W)=>{const S=M.count/O*h,c=d+W*v,k=a-S;e.fillStyle=W===w?n.text:Ct,e.globalAlpha=W===w?1:.7,e.fillRect(c+1,k,Math.max(1,v-2),S),e.globalAlpha=1,W===w&&(e.fillStyle=n.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${M.count}`,c+v/2,k-2))});const V=M=>d+(M-$)/ot*t;e.font="10px system-ui, sans-serif";for(const[M,W]of[[nt,"LSL"],[b,"USL"]]){if(M===void 0)continue;const S=V(M);e.strokeStyle=n.warnBorder,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(S,o),e.lineTo(S,a),e.stroke(),e.setLineDash([]),e.fillStyle=n.warnBorder,e.textAlign="center",e.textBaseline="bottom",e.fillText(W,S,o-2)}e.font="11px system-ui, sans-serif",e.strokeStyle=n.border,e.lineWidth=1,e.beginPath(),e.moveTo(d,a),e.lineTo(d+t,a),e.stroke(),e.fillStyle=n.textMuted,e.textAlign="center",e.textBaseline="top";const C=Math.max(2,Math.floor(t/44)),dt=Math.ceil(s.length/C),ht=Math.max(1,dt);for(let M=0;M<=s.length;M+=ht){const W=M<s.length?s[M].rangeLow:s[s.length-1].rangeHigh,S=d+M*v;e.beginPath(),e.moveTo(S,a),e.lineTo(S,a+4),e.stroke(),e.fillText(Z.tick(W),S,a+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})`,d+t,a+20),e.restore())}g.addEventListener("mousemove",m=>{const u=g.getBoundingClientRect(),e=ct(m.clientX-u.left);if(e!==w&&(w=e,A()),e>=0){const n=s[e];L.innerHTML=`<strong>${st(n.rangeLow,K,"engineering")} \u2013 ${st(n.rangeHigh,K,"engineering")}</strong><br>${n.count} dies`,L.style.display="block",bt(L,i,m.clientX,m.clientY)}else L.style.display="none"}),g.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,A()),L.style.display="none"}),J=xt(i,()=>A()),A()}function kt(r){const{unit:H,limitLow:K,limitHigh:nt}=et(x),b=r.ranges,s=r.series,O=Wt;y&&!s.some(n=>n.groupKey===y)&&(y=null);const E=Math.max(1,...s.flatMap(n=>n.counts)),g=i.ownerDocument.createElement("div");Object.assign(g.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),g.textContent=`${s.length} groups \xB7 max ${E} dies/bucket`,f.appendChild(g);const w=i.ownerDocument.createElement("div");Object.assign(w.style,{display:"flex",flexWrap:"wrap",gap:"4px 12px",marginBottom:"4px"}),s.forEach((n,d)=>{const t=i.ownerDocument.createElement("button");t.type="button",Pt(t,i,L,`${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 h=i.ownerDocument.createElement("span");Object.assign(h.style,{width:"10px",height:"10px",borderRadius:"2px",background:O(d),flex:"0 0 auto"});const o=i.ownerDocument.createElement("span");o.textContent=n.groupKey,t.append(h,o);const a=y!==null&&y!==n.groupKey;t.style.opacity=a?"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()}),w.appendChild(t)}),f.appendChild(w);const p=i.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",f.appendChild(p);const $=window.devicePixelRatio||1,X=b[0].rangeLow,ot=b[b.length-1].rangeHigh,Z=ot-X||1,Y=wt(Math.max(Math.abs(X),Math.abs(ot)),H),ct=()=>g.offsetHeight+w.offsetHeight;let A=-1,m={plotX:tt+36,plotMaxWidth:10,barWidth:10};function u(){Mt(p,ct());const n=f.clientWidth,d=yt(i,f,p,mt);p.width=Math.max(1,Math.floor(n*$)),p.height=Math.max(1,Math.floor(d*$)),p.style.width=`${n}px`,p.style.height=`${d}px`;const t=p.getContext("2d");t.setTransform($,0,0,$,0,0),t.clearRect(0,0,n,d),t.font="11px system-ui, sans-serif";const h=Tt(i),o=tt+36,a=Math.max(10,n-o-tt),v=at,V=d-vt-at,T=v+V,C=a/b.length;m={plotX:o,plotMaxWidth:a,barWidth:C},St(t,o,v,T,a,E,{text:h.textMuted,axis:h.border,grid:h.border}),A>=0&&(t.fillStyle=h.bgHover,t.globalAlpha=.5,t.fillRect(o+A*C,v,C,V),t.globalAlpha=1);const dt=s.map((c,k)=>({s:c,i:k})).sort((c,k)=>(c.s.groupKey===y?1:0)-(k.s.groupKey===y?1:0));for(const{s:c,i:k}of dt){const D=y===c.groupKey,ut=y!==null&&!D,ft=O(k);t.strokeStyle=ft,t.fillStyle=ft,t.lineWidth=D?2:1,t.globalAlpha=ut?.12:y?.32:.22,t.beginPath(),t.moveTo(o,T),c.counts.forEach((gt,it)=>{const Q=o+it*C,q=T-gt/E*V;t.lineTo(Q,q),t.lineTo(Q+C,q)}),t.lineTo(o+a,T),t.closePath(),t.fill(),t.globalAlpha=ut?.3:1,t.beginPath(),c.counts.forEach((gt,it)=>{const Q=o+it*C,q=T-gt/E*V;it===0?t.moveTo(Q,q):t.lineTo(Q,q),t.lineTo(Q+C,q)}),t.stroke()}t.globalAlpha=1;const ht=c=>o+(c-X)/Z*a;t.font="10px system-ui, sans-serif";for(const[c,k]of[[K,"LSL"],[nt,"USL"]]){if(c===void 0)continue;const D=ht(c);t.strokeStyle=h.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(D,v),t.lineTo(D,T),t.stroke(),t.setLineDash([]),t.fillStyle=h.warnBorder,t.textAlign="center",t.textBaseline="bottom",t.fillText(k,D,v-2)}t.font="11px system-ui, sans-serif",t.strokeStyle=h.border,t.lineWidth=1,t.beginPath(),t.moveTo(o,T),t.lineTo(o+a,T),t.stroke(),t.fillStyle=h.textMuted,t.textAlign="center",t.textBaseline="top";const W=Math.max(2,Math.floor(a/44)),S=Math.max(1,Math.ceil(b.length/W));for(let c=0;c<=b.length;c+=S){const k=c<b.length?b[c].rangeLow:b[b.length-1].rangeHigh,D=o+c*C;t.beginPath(),t.moveTo(D,T),t.lineTo(D,T+4),t.stroke(),t.fillText(Y.tick(k),D,T+6)}Y.unitLabel&&(t.save(),t.font="10px system-ui, sans-serif",t.fillStyle=h.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${Y.unitLabel})`,o+a,T+20),t.restore())}function e(n){const{plotX:d,plotMaxWidth:t,barWidth:h}=m;if(n<d||n>d+t)return-1;const o=Math.floor((n-d)/h);return o>=0&&o<b.length?o:-1}p.addEventListener("mousemove",n=>{const d=p.getBoundingClientRect(),t=e(n.clientX-d.left);if(t!==A&&(A=t,u()),t>=0){const h=b[t],o=s.map((a,v)=>`<span style="display:inline-block;width:9px;height:9px;border-radius:2px;background:${O(v)};margin-right:5px;"></span>${a.groupKey}: ${a.counts[t]}`).join("<br>");L.innerHTML=`<strong>${st(h.rangeLow,H,"engineering")} \u2013 ${st(h.rangeHigh,H,"engineering")}</strong><br>${o}`,L.style.display="block",bt(L,i,n.clientX,n.clientY)}else L.style.display="none"}),p.addEventListener("mouseleave",()=>{A!==-1&&(A=-1,u()),L.style.display="none"}),J=xt(i,()=>u()),u()}U();function Lt(r){!F.some(H=>H.testNumber===r)||r===x||(x=r,I.value=String(r),U())}return{card:i,setTest:Lt,destroy:()=>J?.disconnect()}}
|
|
1
|
+
import{buildTestHistogramData as jt,collectTestValues as Ht,buildTestHistogramSeries as _t,testValueExtent as At}from"../../stats/histogram.js";import{SPACE as at,fontPx as Q,FONT as Ct,CLR as $t}from"../toolbar.js";import{fmt as bt}from"../../renderer/fmt.js";import{QUANTITY as Ut,categorical as Xt}from"./palette.js";import{cardShell as Yt,observeResize as Pt,makeTooltip as Qt,attachChartTip as qt,positionChartTooltip as Wt,makeLinkedTestSelect as Jt,makeWaferSelect as Zt,makeLinkedAxisPrefs as te,renderEmptyState as Tt,chartFillHeight as Dt,applyCanvasFlow as Et,makeAxisFormat as It,PADDING as ft,robustFence as Bt,shouldIncludeLimitsByDefault as Ft,drawOffAxisLimits as ee,resolveAxisRange as ne,chartSwatchCss as ie,makeSeriesLegendItem as oe,prepareCanvas as Kt}from"./chartShell.js";const St=230,Ot=36,yt=18;function Rt(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}=Yt(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=Jt(rt,d,s=>{d=s,j(),l.onTestChange?.(s)},{maxWidth:"200px",ownerDocument:r.ownerDocument}),ct=V.el;Z.appendChild(ct);const wt=Zt(q,tt,s=>{tt=s,j()},{ownerDocument:r.ownerDocument});Z.appendChild(wt);function Nt(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=te(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}=At(o,d);if(it=s??Ft(e,p,a,m),v){const x=Ht(o,d),H=Bt(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?_t(B,d,16,it?et(d).limitLow:void 0,it?et(d).limitHigh:void 0,u):null;if(wt.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}zt($);return}const _=tt!==null?[q[tt]]:q,{unit:O,limitLow:P,limitHigh:W}=et(d),k=Ht(_,d),{min:ot,max:mt}=At(_,d),ut=s??Ft(ot,mt,P,W);gt.sync(ut,P!==void 0||W!==void 0);const U=v?Bt(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=jt(_,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:Ct.body,color:$t.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=It(Math.max(Math.abs(n),Math.abs(g)),O);function c(a){const m=ft+36,o=M.clientWidth-m-ft,e=a-Ot-yt;return{plotX:m,plotMaxWidth:Math.max(10,o),plotMaxHeight:e,plotTop:yt}}function f(a){const{plotX:m,plotMaxWidth:o}=c(St);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(){Et(M,Y);const a=y.clientWidth,m=Dt(r,y,M,St),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;Rt(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,vt=x+z*w,kt=i-I;e.fillStyle=z===F?p.text:Ut,e.globalAlpha=z===F?1:.7,e.fillRect(vt+1,kt,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}`,vt+w/2,kt-2))});const A=b=>x+(b-n)/L*H;e.font=`${Q(-1)}px system-ui, sans-serif`;const{offAxis:ht}=ne({dataMin:n,dataMax:g,limitLow:P,limitHigh:W,includeLimits:!1});ee(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>${bt(e.rangeLow,O,"engineering")} \u2013 ${bt(e.rangeHigh,O,"engineering")}</strong><br>${e.count} dies`,C.style.display="block",Wt(C,r,a.clientX,a.clientY)}else C.style.display="none"}),M.addEventListener("mouseleave",()=>{F!==-1&&(F=-1,T()),C.style.display="none"}),dt=Pt(r,()=>T()),T()}function zt(s){const{unit:v,limitLow:nt,limitHigh:it}=et(d),u=s.ranges,$=s.series,_=Xt;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:Ct.body,color:$t.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=oe(n.groupKey,_(g),r.ownerDocument),t=L.el;qt(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;Nt(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=It(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(){Et(k,h());const n=y.clientWidth,g=Dt(r,y,k,St),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-Ot-yt,o=a+m,e=T/u.length;Y={plotX:f,plotMaxWidth:T,barWidth:e},Rt(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="${ie(_(T))};margin-right:4px"></span>${f.groupKey}: ${f.counts[L]}`).join("<br>");C.innerHTML=`<strong>${bt(t.rangeLow,v,"engineering")} \u2013 ${bt(t.rangeHigh,v,"engineering")}</strong><br>${c}`,C.style.display="block",Wt(C,r,n.clientX,n.clientY)}else C.style.display="none"}),k.addEventListener("mouseleave",()=>{D!==-1&&(D=-1,M()),C.style.display="none"}),dt=Pt(r,()=>M()),M()}j();function Gt(s){V.set(s)&&(d=s,j())}function Vt(s){gt.set(s)&&j()}return{card:r,setTest:Gt,setAxisPrefs:Vt,destroy:()=>dt?.disconnect()}}
|
|
@@ -6,7 +6,6 @@ export interface RegionYieldDiagramOptions {
|
|
|
6
6
|
mode: RegionYieldMode;
|
|
7
7
|
/** Ring mode: ordered ring 1 (core) → ring N (edge), matching `buildRegionYieldData`'s output for `buildRingRegions`. Quadrant mode: any order — each row's quadrant is read from its `key` (`quadrant:NE` etc.), not position. */
|
|
8
8
|
rows: RegionYieldDatum[];
|
|
9
|
-
colorScheme?: string;
|
|
10
9
|
onSaveImage?: SaveImageHandler;
|
|
11
10
|
/** Document to build this panel's DOM into. Default `document` — pass the
|
|
12
11
|
* host's own `ownerDocument` when the container might live in a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseRegionKey as
|
|
1
|
+
import{parseRegionKey as E}from"../../stats/regions.js";import{yieldFill as A}from"./palette.js";import{fontPx as T}from"../toolbar.js";import{cardShell as D,resolveChartCanvasColors as N,observeResize as F,growCardToFitContent as $,renderEmptyState as j,prepareCanvas as z}from"./chartShell.js";const v=320,O=220,_={SE:[0,Math.PI/2],SW:[Math.PI/2,Math.PI],NW:[Math.PI,1.5*Math.PI],NE:[1.5*Math.PI,2*Math.PI]};function q(e,a,n,r,l,o){e.beginPath(),e.moveTo(a+o,n),e.arcTo(a+r,n,a+r,n+l,o),e.arcTo(a+r,n+l,a,n+l,o),e.arcTo(a,n+l,a,n,o),e.arcTo(a,n,a+r,n,o),e.closePath()}function w(e,a,n,r,l,o){e.font=`${T(-1)}px system-ui, sans-serif`;const h=e.measureText(r).width;e.font="700 11px system-ui, sans-serif";const M=e.measureText(l).width,g=Math.max(h,M)+12,b=30;e.fillStyle=o.bg,q(e,a-g/2,n-b/2,g,b,4),e.fill(),e.strokeStyle=o.border,e.lineWidth=1,e.stroke(),e.textAlign="center",e.textBaseline="middle",e.fillStyle=o.textMuted,e.font=`${T(-1)}px system-ui, sans-serif`,e.fillText(r,a,n-7),e.fillStyle=o.text,e.font="700 11px system-ui, sans-serif",e.fillText(l,a,n+7)}export function renderRegionYieldDiagram(e){const{mode:a,rows:n,onSaveImage:r}=e,l=e.title??(a==="ring"?"Ring yield":"Quadrant yield"),{card:o,body:h}=D(l,r,e.ownerDocument),M=n.length>0;if(!M)return j(h,"No bin data available for regional yield."),{card:o,hasData:M,destroy:()=>{}};Object.assign(h.style,{display:"flex",justifyContent:"center",alignItems:"center"});const g=o.ownerDocument.createElement("canvas");Object.assign(g.style,{display:"block",flex:"none"}),h.appendChild(g);function b(){const u=Math.max(O,Math.min(h.clientWidth||v,v));$(o,h,u);const S=z(g,o,u,u);if(!S)return;const{ctx:t}=S,y=N(o),k=A,c=u/2,d=u/2,f=u/2-34;if(a==="ring"){const i=n.length;for(let s=0;s<i;s++){const m=s/i*f,P=(s+1)/i*f;t.beginPath(),t.arc(c,d,P,0,2*Math.PI),t.arc(c,d,m,0,2*Math.PI,!0),t.closePath(),t.fillStyle=k(n[s].yieldPercent),t.fill(),t.strokeStyle=y.border,t.lineWidth=1,t.stroke()}t.beginPath(),t.arc(c,d,f,0,2*Math.PI),t.strokeStyle=y.text,t.lineWidth=1.5,t.stroke();for(let s=0;s<i;s++){const m=(s+.5)/i*f,P=-Math.PI/2+s/i*2*Math.PI,I=c+Math.cos(P)*m,p=d+Math.sin(P)*m;w(t,I,p,n[s].label,`${n[s].yieldPercent.toFixed(1)}%`,y)}}else{for(const i of n){const s=E(i.key).quadrant,m=s?_[s]:void 0;if(!m)continue;const[P,I]=m;t.beginPath(),t.moveTo(c,d),t.arc(c,d,f,P,I),t.closePath(),t.fillStyle=k(i.yieldPercent),t.fill(),t.strokeStyle=y.border,t.lineWidth=1,t.stroke();const p=(P+I)/2,R=c+Math.cos(p)*f*.6,W=d+Math.sin(p)*f*.6;w(t,R,W,i.label,`${i.yieldPercent.toFixed(1)}%`,y)}t.beginPath(),t.arc(c,d,f,0,2*Math.PI),t.strokeStyle=y.text,t.lineWidth=1.5,t.stroke()}}const C=F(o,()=>b());return b(),{card:o,hasData:M,destroy:()=>C.disconnect()}}
|
|
@@ -11,7 +11,12 @@ export interface ScatterPanelOptions {
|
|
|
11
11
|
testDefs: TestDef[];
|
|
12
12
|
xTestNumber?: number;
|
|
13
13
|
yTestNumber?: number;
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The host map's resolved hard-bin colours (`View.binColors.hard`), so a
|
|
16
|
+
* point is the colour its die is on the map. Omitted ⇒ points without a
|
|
17
|
+
* group colour take the no-data fill.
|
|
18
|
+
*/
|
|
19
|
+
binColors?: ReadonlyMap<number, string>;
|
|
15
20
|
onSaveImage?: SaveImageHandler;
|
|
16
21
|
/**
|
|
17
22
|
* When the Analysis tab's "Group by" is active, every group's points are
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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()}}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TestPassRateItem } from '../../stats/testPassRate.js';
|
|
2
|
+
import type { TestDef } from '../../renderer/buildWaferMap.js';
|
|
3
|
+
import { type SaveImageHandler } from './chartShell.js';
|
|
4
|
+
export interface TestPassRatePanelOptions {
|
|
5
|
+
title?: string;
|
|
6
|
+
/** One entry per group. Pass a single group for the ungrouped case. */
|
|
7
|
+
groups: {
|
|
8
|
+
key: string;
|
|
9
|
+
items: TestPassRateItem[];
|
|
10
|
+
}[];
|
|
11
|
+
testDefs: TestDef[] | undefined;
|
|
12
|
+
onSaveImage?: SaveImageHandler;
|
|
13
|
+
ownerDocument?: Document;
|
|
14
|
+
}
|
|
15
|
+
export interface TestPassRatePanelHandle {
|
|
16
|
+
card: HTMLElement;
|
|
17
|
+
destroy: () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function renderTestPassRatePanel(options: TestPassRatePanelOptions): TestPassRatePanelHandle;
|
|
20
|
+
//# sourceMappingURL=testPassRate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{buildTestPassRateData as E,hasJudgeableTests as F}from"../../stats/testPassRate.js";import{categorical as B,QUANTITY as I}from"./palette.js";import{SPACE as N,FONT as _,CLR as T}from"../toolbar.js";import{cardShell as A,makeTooltip as H,makeSegmented as O,renderEmptyState as w}from"./chartShell.js";import{renderGroupedBarPlot as j}from"./groupedBarPlot.js";const M=100,k=8,G={spec:"Spec limits",testFlag:"Tester flag",functional:"Functional"},U={spec:"Parametric pass rate \xB7 spec limits",testFlag:"Parametric pass rate \xB7 tester flag",functional:"Functional test pass rate"};export function renderTestPassRatePanel(g){const{groups:f,testDefs:u,onSaveImage:y}=g,p=g.ownerDocument??document,$=f.flatMap(e=>e.items.flatMap(a=>a.dies??[])),d=["spec","testFlag","functional"].filter(e=>F(u,e,$));let s=d[0]??"spec";const b=e=>U[e],{card:o,heading:P,body:i,controlsRow:v}=A(b(s),y,p);o.style.alignSelf="start",d.length>1&&v.appendChild(O(d.map(e=>[e,G[e]]),s,e=>{s=e,P.textContent=b(s),D()},p));const n=p.createElement("div");Object.assign(n.style,{color:T.label,fontSize:_.body,marginBottom:N.sm}),o.insertBefore(n,i);const h=H(o);let m=null;function D(){if(m?.destroy(),m=null,i.innerHTML="",d.length===0){n.textContent="",w(i,"No tests with a pass/fail verdict \u2014 parametric tests need spec limits, functional tests need recorded results.");return}const e=E(f,u,s);if(e.rows.length===0){n.textContent="",w(i,s==="spec"?"No parametric test has spec limits to judge against.":s==="testFlag"?"No parametric test has a recorded tester verdict.":"No functional test has recorded results.");return}const a=e.groups.length>1,x=s==="spec"?"Dies within spec limits, per test":s==="testFlag"?"Dies the tester marked pass, per test":"Dies passing the recorded verdict, per test";if(n.textContent=x+(a?" \xB7 one bar per group":"")+" \xB7 worst first",e.disagreementDies){const t=p.createElement("span");t.textContent=` \xB7 ${e.disagreementDies.toLocaleString()} die${e.disagreementDies===1?"":"s"} judged differently by limits and tester flag`,Object.assign(t.style,{color:T.warnText}),n.appendChild(t)}const L=e.rows,R=t=>a?B(t):I;m=j(o,i,h,{rows:L.map(t=>({label:t.label,bars:t.byGroup.map(l=>({fraction:l.passRatePercent===null?null:l.passRatePercent/100})),trailing:t.overall.passRatePercent===null?"\u2014":`${t.overall.passRatePercent.toFixed(1)}%`})),groups:e.groups,labelWidth:M,maxVisibleRows:k,showLegend:a,maxLabelChars:14,emptyBarText:"no data",colorOf:R,tooltipHtml:(t,l)=>{const r=L[t],c=r.byGroup[l],S=a?` \xB7 ${e.groups[l]}`:"";if(c.passRatePercent===null)return`<strong>${r.label}</strong>${S}<br>no dies with a verdict`;const C=s==="spec"&&!a&&(r.failLowDies||r.failHighDies)?`<br>${r.failLowDies??0} below LSL \xB7 ${r.failHighDies??0} above USL`:"";return`<strong>${r.label}</strong>${S}<br>${c.passRatePercent.toFixed(1)}% pass<br>${c.passDies.toLocaleString()} pass \xB7 ${c.failDies.toLocaleString()} fail \xB7 n = ${c.totalDies.toLocaleString()}`+C}})}return D(),{card:o,destroy:()=>{m?.destroy(),h.remove(),o.remove()}}}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type TrendItem } from '../../stats/trend.js';
|
|
2
|
+
import { type TestDef } from '../../renderer/buildWaferMap.js';
|
|
3
|
+
import { type AxisPrefs, type SaveImageHandler } from './chartShell.js';
|
|
4
|
+
export interface TrendPanelOptions {
|
|
5
|
+
title?: string;
|
|
6
|
+
items: TrendItem[];
|
|
7
|
+
testDefs: TestDef[];
|
|
8
|
+
selectedTestNumber?: number;
|
|
9
|
+
onSaveImage?: SaveImageHandler;
|
|
10
|
+
/** Click a point to open that wafer's map on this test. */
|
|
11
|
+
onOpen?: (key: number, testNumber: number) => void;
|
|
12
|
+
/**
|
|
13
|
+
* What the dashed reference line is, as a noun phrase — "lot mean" when the
|
|
14
|
+
* items are one lot, else "mean of all wafers" (see stats/population.ts).
|
|
15
|
+
* The panel sees no wafer metadata, so the host names it. Default
|
|
16
|
+
* "mean of all wafers", which is true of any set.
|
|
17
|
+
*/
|
|
18
|
+
centreLabel?: string;
|
|
19
|
+
/** Initial axis toggles, shared with the sibling distribution panels. */
|
|
20
|
+
axisPrefs?: AxisPrefs;
|
|
21
|
+
/** Fired when the user changes an axis toggle here, so siblings can follow. */
|
|
22
|
+
onAxisPrefsChange?: (prefs: AxisPrefs) => void;
|
|
23
|
+
/** Fired when the USER picks a test here, so siblings can follow. Not fired
|
|
24
|
+
* by `setTest`, which is how a broadcast avoids bouncing back. */
|
|
25
|
+
onTestChange?: (testNumber: number) => void;
|
|
26
|
+
ownerDocument?: Document;
|
|
27
|
+
}
|
|
28
|
+
export interface TrendPanelHandle {
|
|
29
|
+
card: HTMLElement;
|
|
30
|
+
destroy: () => void;
|
|
31
|
+
/** Cross-panel link — switch the displayed test in place. */
|
|
32
|
+
setTest: (testNumber: number) => void;
|
|
33
|
+
/** Adopt the shared axis toggles (see `AxisPrefs`). */
|
|
34
|
+
setAxisPrefs: (prefs: AxisPrefs) => void;
|
|
35
|
+
}
|
|
36
|
+
export declare function renderTrendPanel(options: TrendPanelOptions): TrendPanelHandle;
|
|
37
|
+
//# sourceMappingURL=trend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{buildTestTrendData as de,trendCentre as fe}from"../../stats/trend.js";import{isParametricTest as ue}from"../../renderer/buildWaferMap.js";import{fmt as D}from"../../renderer/fmt.js";import{SPACE as ie,fontPx as he,FONT as pe,CLR as ge}from"../toolbar.js";import{QUANTITY as _}from"./palette.js";import{cardShell as xe,observeResize as be,makeTooltip as ye,positionChartTooltip as ve,makeLinkedTestSelect as Te,makeLinkedAxisPrefs as Le,renderEmptyState as z,growCardToFitContent as we,chartFillHeight as ke,PADDING as X,resolveAxisRange as Ce,shouldIncludeLimitsByDefault as Se,drawOffAxisLimits as Ae,prepareCanvas as Pe}from"./chartShell.js";const oe=220,Ne=56,De=34,Me=3.5;export function renderTrendPanel(g){const{items:j,testDefs:le,onSaveImage:se,onOpen:C,centreLabel:G="mean of all wafers"}=g,re=g.title??"Wafer-to-wafer trend",{card:l,body:f,controlsRow:U}=xe(re,se,g.ownerDocument);l.style.alignSelf="start";const M=le.filter(o=>o.testNumber!==void 0&&ue(o));let h=g.selectedTestNumber??M[0]?.testNumber??null,I=0;const Y=Te(M,h,o=>{h=o,S(),g.onTestChange?.(o)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:l.ownerDocument});U.appendChild(Y.el);const O=l.ownerDocument.createElement("span");Object.assign(O.style,{display:"inline-flex",gap:ie.lg,alignItems:"center"}),U.appendChild(O);const $=Le(O,g.axisPrefs,o=>{g.onAxisPrefsChange?.(o),S()},l.ownerDocument),T=l.ownerDocument.createElement("div");Object.assign(T.style,{color:ge.label,fontSize:pe.body,marginBottom:ie.sm}),l.insertBefore(T,f);const L=ye(l);let H=null,n=[];function ae(o){return M.find(B=>B.testNumber===o)}function S(){const{includeLimits:o,clipOutliers:B}=$.get();if(f.innerHTML="",H?.disconnect(),H=null,M.length===0||h===null){T.textContent="",z(f,"No parametric test data available for a trend.");return}if(j.length<2){T.textContent="",z(f,"A wafer-to-wafer trend needs at least two wafers.");return}if(n=de(j,h),!n.some(r=>r.count>0)){T.textContent="",z(f,"No values recorded for this test.");return}const s=ae(h),x=fe(n),Q=()=>{T.textContent=`Point = wafer mean, whisker = \xB11\u03C3, dashed = ${G} \xB7 wafers in slot order`+(C?" \xB7 click a point to open that wafer":"")+(I?` \xB7 axis clipped, ${I} outside`:"")};Q();const u=l.ownerDocument.createElement("canvas");u.style.display="block",f.appendChild(u);let A=null;const q=()=>{const r=Math.max(1,f.clientWidth),d=Math.max(oe,ke(l,f,u,oe)),i=Pe(u,l,r,d);if(!i)return;const{ctx:e,theme:b}=i;let P=1/0,N=-1/0;const K=[];for(const t of n)!t.count||!Number.isFinite(t.mean)||(P=Math.min(P,t.mean-t.stddev),N=Math.max(N,t.mean+t.stddev),K.push(t.mean-t.stddev,t.mean+t.stddev));if(!Number.isFinite(P)||!Number.isFinite(N))return;const V=o??Se(P,N,s?.limitLow,s?.limitHigh);$.sync(V,s?.limitLow!==void 0||s?.limitHigh!==void 0);const E=Ce({dataMin:P,dataMax:N,limitLow:s?.limitLow,limitHigh:s?.limitHigh,includeLimits:V,clipOutliers:B,values:K});I=E.clippedCount,Q();let p=E.lo,w=E.hi;const Z=(w-p)*.08;p-=Z,w+=Z;const a=X+Ne;e.font=`${he(-1)}px system-ui, sans-serif`;const ee=t=>t.length>8?`${t.slice(0,7)}\u2026`:t,ce=n.length?Math.ceil(Math.max(e.measureText(ee(n[0].label)).width,e.measureText(ee(n[n.length-1].label)).width)/2):0,y=r-X-ce,F=X,k=d-De,v=t=>k-(t-p)/(w-p)*(k-F),te=n.length>1?(y-a)/(n.length-1):0;A={left:a,step:te};const R=t=>n.length>1?a+t*te:(a+y)/2;e.strokeStyle=b.border,e.lineWidth=1,e.beginPath(),e.moveTo(a,F),e.lineTo(a,k),e.lineTo(y,k),e.stroke(),e.fillStyle=b.text,e.textAlign="right",e.textBaseline="middle";for(const t of[0,.5,1]){const c=p+(w-p)*t;e.fillText(D(c,s?.unit),a-6,v(c))}Ae(e,E.offAxis,{left:a,right:y,top:F,bottom:k},"vertical",b.limitLine,t=>D(t,s?.unit));for(const[t,c]of[[s?.limitLow,"LSL"],[s?.limitHigh,"USL"]]){if(t===void 0||t<p||t>w)continue;const m=v(t);e.save(),e.strokeStyle=b.limitLine,e.globalAlpha=.7,e.setLineDash([4,3]),e.beginPath(),e.moveTo(a,m),e.lineTo(y,m),e.stroke(),e.restore(),e.save(),e.fillStyle=b.limitLine,e.textAlign="left",e.fillText(c,a+3,m-6),e.restore()}if(x!==null&&x>=p&&x<=w){const t=v(x);e.save(),e.strokeStyle=b.text,e.globalAlpha=.5,e.setLineDash([3,3]),e.beginPath(),e.moveTo(a,t),e.lineTo(y,t),e.stroke(),e.restore()}e.strokeStyle=_,e.lineWidth=1.5,e.beginPath();let W=!1;n.forEach((t,c)=>{if(!t.count||!Number.isFinite(t.mean)){W=!1;return}const m=R(c),ne=v(t.mean);W?e.lineTo(m,ne):e.moveTo(m,ne),W=!0}),e.stroke(),n.forEach((t,c)=>{if(!t.count||!Number.isFinite(t.mean))return;const m=R(c);t.stddev>0&&(e.save(),e.strokeStyle=_,e.globalAlpha=.55,e.lineWidth=1,e.beginPath(),e.moveTo(m,v(t.mean-t.stddev)),e.lineTo(m,v(t.mean+t.stddev)),e.stroke(),e.restore()),e.fillStyle=_,e.beginPath(),e.arc(m,v(t.mean),Me,0,Math.PI*2),e.fill()}),e.fillStyle=b.text,e.textBaseline="top",e.textAlign="center";const me=Math.max(1,Math.ceil(n.length*42/Math.max(1,y-a)));n.forEach((t,c)=>{if(c%me!==0&&c!==n.length-1)return;const m=t.label.length>8?`${t.label.slice(0,7)}\u2026`:t.label;e.fillText(m,R(c),k+6)}),we(l,f,d)},J=r=>{if(!A||A.step<=0)return-1;const d=u.getBoundingClientRect(),i=Math.round((r.clientX-d.left-A.left)/A.step);return i>=0&&i<n.length&&n[i].count>0?i:-1};u.addEventListener("mousemove",r=>{const d=J(r);if(d<0){L.style.display="none",u.style.cursor="default";return}const i=n[d];u.style.cursor=C&&i.key!==void 0?"pointer":"default",L.innerHTML=`<strong>${i.label}</strong><br>mean ${D(i.mean,s?.unit)} \xB7 \u03C3 ${D(i.stddev,s?.unit)}<br>n = ${i.count.toLocaleString()}`+(x!==null?`<br>\u0394 vs ${G} ${i.mean-x>=0?"+":""}${D(i.mean-x,s?.unit)}`:"")+(C&&i.key!==void 0?"<br><em>click to open this wafer</em>":""),L.style.display="block",ve(L,l,r.clientX,r.clientY)}),u.addEventListener("mouseleave",()=>{L.style.display="none"}),u.addEventListener("click",r=>{const d=J(r);if(d<0||!C)return;const i=n[d];i.key!==void 0&&h!==null&&C(i.key,h)}),q(),H=be(f,q)}return S(),{card:l,destroy:()=>{H?.disconnect(),L.remove(),l.remove()},setTest:o=>{Y.set(o)&&(h=o,S())},setAxisPrefs:o=>{$.set(o)&&S()}}}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import{hasPosition as
|
|
2
|
-
.wmap-dielist-table { width: 100%; border-collapse: collapse; font-size:
|
|
1
|
+
import{hasPosition as R,isPositionedDie as O}from"../core/dies.js";import{classifyDie as z}from"../core/index.js";import{isParametricTest as B,getTestPassStatus as x}from"../renderer/buildWaferMap.js";import{resolveMetadataColumns as V}from"../stats/metadataColumns.js";import{LEADING as D,TRACKING as W,wireControlHover as G,controlStyle as K,SPACE as X,RADIUS as F,FONT as C,CLR as d,saveTextFile as Y}from"./toolbar.js";import{csvField as I}from"./summaryPanel.js";import{fmt as T}from"../renderer/fmt.js";const _=5e4,k=new WeakSet;function Q(n){if(k.has(n))return;k.add(n);const o=n.createElement("style");o.textContent=`
|
|
2
|
+
.wmap-dielist-table { width: 100%; border-collapse: collapse; font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
3
3
|
.wmap-dielist-th {
|
|
4
4
|
text-align: left; padding: 4px 8px; position: sticky; top: 0;
|
|
5
|
-
background: ${
|
|
6
|
-
border-bottom: 1px solid ${
|
|
5
|
+
background: ${d.panelBg}; color: ${d.label}; font-weight: 600;
|
|
6
|
+
border-bottom: 1px solid ${d.menuBorder}; white-space: nowrap;
|
|
7
7
|
}
|
|
8
8
|
.wmap-dielist-td {
|
|
9
|
-
padding:
|
|
10
|
-
border-bottom: 1px solid ${
|
|
9
|
+
padding: 4px 8px; color: ${d.text};
|
|
10
|
+
border-bottom: 1px solid ${d.menuBorder}; white-space: nowrap;
|
|
11
11
|
}
|
|
12
|
-
`,n.head.appendChild(o)}function
|
|
12
|
+
`,n.head.appendChild(o)}function c(n,o,t,a){const i=n.createElement(o);return t&&Object.assign(i.style,t),a!==void 0&&(i.textContent=a),i}function U(n){return R(n)?String(n.x):""}function q(n){return R(n)?String(n.y):""}function J(n,o){if(o!==void 0)return o.filter(a=>a.testNumber!==void 0);const t=new Map;for(const a of n)for(const i of Object.keys(a.testValues??{})){const s=Number(i);t.has(s)||t.set(s,{testNumber:s,name:`Test ${s}`})}return[...t.values()].sort((a,i)=>a.testNumber-i.testNumber)}function Z(n,o,t){if(!o)return;const a=new Map;for(const i of n){if(!O(i))continue;const s=o(i);s&&a.set(i,z(i,s,{ringCount:t}))}return a.size>0?a:void 0}export function buildDieListSection(n,o,t={}){if(!n.length)return null;const a=t.ownerDocument??document;Q(a);const i=J(n,o),s=t.maxRows??_,S=Math.max(0,n.length-s),H=S>0?n.slice(0,s):n,m=c(a,"div",{display:"flex",flexDirection:"column",gap:X.md,flex:"1",minHeight:"0",minWidth:"0"}),w=c(a,"div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});w.appendChild(c(a,"div",{fontSize:C.meta,fontWeight:"700",letterSpacing:W,textTransform:"uppercase",color:d.label},t.title??`Die list (${n.length})`));const g=c(a,"button",{fontSize:C.body,padding:"3px 8px",borderRadius:F.control,...K("outlined"),background:d.menuBg},"Export CSV");g.type="button",G(g),w.appendChild(g),m.appendChild(w),t.note&&m.appendChild(c(a,"div",{fontSize:C.body,color:d.label,lineHeight:D.base,flexShrink:"0"},t.note));const v=Z(n,t.getWafer,t.ringCount??4),P=n.some(e=>e.edgeExcluded===!0),j=[...t.extraColumn?[t.extraColumn.label]:[],"X","Y","Ring","Quadrant","Edge excluded","Site","Hard bin","Soft bin",...i.map(e=>e.unit?`${e.name} (${e.unit})`:e.name)],{columns:M,truncatedKeys:h}=V({dies:n,waferMetadata:t.waferMetadata,metadataFields:t.metadataFields,dieKeys:t.metadataColumns,waferKeys:t.waferMetadataColumns==="none"?"none":void 0,waferPlacement:t.waferMetadataColumns??"csv",reservedLabels:j}),y=[...t.extraColumn?[{label:t.extraColumn.label,get:e=>t.extraColumn.get(e)??""}]:[],{label:"X",get:U},{label:"Y",get:q},...v?[{label:"Ring",get:e=>{const l=v.get(e);return l?String(l.ring):""}},{label:"Quadrant",get:e=>v.get(e)?.quadrant??""}]:[],...P?[{label:"Edge excluded",get:e=>e.edgeExcluded?"Yes":"No"}]:[],{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):""},...i.map(e=>{const l=!B(e);return{label:e.unit?`${e.name} (${e.unit})`:e.name,get:r=>{if(l){const p=x(r,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=r.testValues?.[e.testNumber];if(u!==void 0)return T(u,e.unit);const b=x(r,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"},csvGet:r=>{if(l){const p=x(r,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=r.testValues?.[e.testNumber];if(u!==void 0)return T(u,void 0,"engineering");const b=x(r,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"}}}),...M.map(e=>({label:e.label,get:e.get,csvOnly:e.csvOnly}))],N=y.filter(e=>!e.csvOnly),$=c(a,"div",{overflow:"auto",flex:"1",minHeight:"0",minWidth:"0",border:`1px solid ${d.menuBorder}`,borderRadius:F.control,...t.maxHeight?{maxHeight:t.maxHeight}:{}}),f=a.createElement("table");f.className="wmap-dielist-table",f.setAttribute("aria-label",t.title??`Die list (${n.length} dies)`);const E=c(a,"thead"),A=c(a,"tr");for(const e of N){const l=a.createElement("th");l.className="wmap-dielist-th",l.scope="col",l.textContent=e.label,A.appendChild(l)}E.appendChild(A),f.appendChild(E);const L=c(a,"tbody");for(const e of H){const l=c(a,"tr");for(const r of N){const u=a.createElement("td");u.className="wmap-dielist-td",u.textContent=r.get(e),l.appendChild(u)}L.appendChild(l)}if(f.appendChild(L),$.appendChild(f),m.appendChild($),S>0||h.length>0){const e=[];S>0&&e.push(`Showing the first ${s.toLocaleString()} of ${n.length.toLocaleString()} dies. The CSV export contains all ${n.length.toLocaleString()}.`),h.length>0&&e.push(`${h.length} further metadata field${h.length===1?"":"s"} not shown.`),m.appendChild(c(a,"div",{fontSize:C.body,color:d.label,lineHeight:D.base,flexShrink:"0"},e.join(" ")))}return g.addEventListener("click",()=>{const e=[y.map(l=>I(l.label)).join(",")];for(const l of n)e.push(y.map(r=>I((r.csvGet??r.get)(l))).join(","));Y(e.join(`
|
|
13
13
|
`),t.csvFilename??"dies.csv","text/csv",t.onSaveText)}),m}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dimensions of the uniform grid toCanvas indexes die centres into for O(1)
|
|
3
|
+
* hit-testing. Cells are nominally `cellW`×`cellH` (1.5 dies across), but the
|
|
4
|
+
* grid only accelerates hit-testing, so its size must never depend on the
|
|
5
|
+
* geometry being sane. A die size wrong by orders of magnitude — a misread
|
|
6
|
+
* WCR record gave dies ~1e-8 mm wide against a normal die-index span — once
|
|
7
|
+
* asked for more cells than an array can hold: every render threw `Invalid
|
|
8
|
+
* array length`, the map never appeared, and a host waiting for it stayed busy
|
|
9
|
+
* forever.
|
|
10
|
+
*
|
|
11
|
+
* So: a non-finite or non-positive cell size falls back to the span, and cells
|
|
12
|
+
* grow until the grid holds at most max(4096, 4 × pointCount) of them. A
|
|
13
|
+
* coarser grid only means a hit test inspects a few more dies; correctness is
|
|
14
|
+
* unaffected, because the caller clamps every die into range.
|
|
15
|
+
*/
|
|
16
|
+
export declare function hitGridDims(spanX: number, spanY: number, cellW: number, cellH: number, pointCount: number): {
|
|
17
|
+
cellW: number;
|
|
18
|
+
cellH: number;
|
|
19
|
+
nCols: number;
|
|
20
|
+
nRows: number;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=hitGrid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const I=4096;export function hitGridDims(h,m,n,a,u){const r=Number.isFinite(h)&&h>0?h:0,c=Number.isFinite(m)&&m>0?m:0;let e=Number.isFinite(n)&&n>0?n:Math.max(r,1),s=Number.isFinite(a)&&a>0?a:Math.max(c,1);const o=Math.max(4096,u*4),M=()=>[Math.max(1,Math.ceil(r/e)+1),Math.max(1,Math.ceil(c/s)+1)];let[t,i]=M();if(t*i>o){const x=Math.sqrt(t*i/o);for(e*=x,s*=x,[t,i]=M();t*i>o;)e*=1.1,s*=1.1,[t,i]=M()}return{cellW:e,cellH:s,nCols:t,nRows:i}}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { WaferMetadata } from '../core/metadata.js';
|
|
2
|
+
export interface IdentityHeaderLotStack {
|
|
3
|
+
lotSize: number;
|
|
4
|
+
aggrMethod?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface IdentityHeaderOptions {
|
|
7
|
+
/** Present when the host result is a lot-stack aggregation rather than a
|
|
8
|
+
* single wafer — the metadata panel then leads with wafer count + method
|
|
9
|
+
* instead of a (nonexistent) single waferId. */
|
|
10
|
+
lotStack?: IdentityHeaderLotStack;
|
|
11
|
+
/** Document to build into. Default `document` — pass the render's own
|
|
12
|
+
* `ownerDocument` when the container might live in a different document
|
|
13
|
+
* (e.g. a gallery card detached into its own popup window). */
|
|
14
|
+
ownerDocument?: Document;
|
|
15
|
+
}
|
|
16
|
+
export interface IdentityHeaderController {
|
|
17
|
+
/** The header row itself: label + chevron (chevron only appears once
|
|
18
|
+
* there's something to expand). Mount wherever the header belongs —
|
|
19
|
+
* normal in-flow layout, its own height never changes. */
|
|
20
|
+
wrap: HTMLDivElement;
|
|
21
|
+
/**
|
|
22
|
+
* The expandable metadata panel. Always created (so callers have a stable
|
|
23
|
+
* element to place), but only ever visible once there's lot-stack context
|
|
24
|
+
* or a metadata field to show. Absolutely positioned (`top:0;left:0;
|
|
25
|
+
* right:0`) — mount it into a `position: relative` ancestor that should be
|
|
26
|
+
* covered while expanded (e.g. the canvas area below the header). Never
|
|
27
|
+
* changes `wrap`'s own height, so expanding never triggers a reflow/resize
|
|
28
|
+
* of anything else.
|
|
29
|
+
*/
|
|
30
|
+
metaPanel: HTMLDivElement;
|
|
31
|
+
/** Re-render with a new label / metadata / lot-stack context. */
|
|
32
|
+
update(label: string, metadata: WaferMetadata | null | undefined, lotStack?: IdentityHeaderLotStack): void;
|
|
33
|
+
/** True when there is nothing at all to show (no label text, no metadata,
|
|
34
|
+
* no lot-stack) — the caller should not mount `wrap`/`metaPanel`. */
|
|
35
|
+
isEmpty(): boolean;
|
|
36
|
+
/** Force the metadata panel closed (chevron back to ▾) — for a caller that
|
|
37
|
+
* needs to cover `metaPanel` with something else (e.g. the Insights tab)
|
|
38
|
+
* and must not leave it expanded-but-hidden, which would desync the
|
|
39
|
+
* chevron/aria state from `metaPanel`'s own visibility. No-op if already
|
|
40
|
+
* collapsed. */
|
|
41
|
+
collapse(): void;
|
|
42
|
+
destroy(): void;
|
|
43
|
+
}
|
|
44
|
+
export declare function collapsedLabel(meta: WaferMetadata, lotStack: IdentityHeaderLotStack | undefined): string | undefined;
|
|
45
|
+
export declare function createIdentityHeader(label: string, metadata: WaferMetadata | null | undefined, opts?: IdentityHeaderOptions): IdentityHeaderController;
|
|
46
|
+
//# sourceMappingURL=identityHeader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{metadataEntries as C,buildCompactMetadataRows as W}from"./summaryPanel.js";import{prettyKey as A}from"../stats/facets.js";import{SHADOW as L,LEADING as k,SPACE as w,FONT as x,CLR as E,Z_ABOVE as M,wireExpandToggle as j}from"./toolbar.js";export function collapsedLabel(r,a){if(a)return a.aggrMethod?`${a.lotSize} wafers \xB7 ${a.aggrMethod}`:`${a.lotSize} wafers`;if(r.lot&&r.waferId!==void 0&&r.waferId!==""){const s=String(r.lot),t=String(r.waferId);return t.includes(s)?t:`${s} \xB7 ${t}`}for(const s of["lot","waferId","product","testProgram"]){const t=r[s];if(t!=null&&t!=="")return String(t)}}export function createIdentityHeader(r,a,s={}){let t=r,b=a??{},o=s.lotStack,i=!1,m=!1;const c=s.ownerDocument??document,e=c.createElement("div");Object.assign(e.style,{display:"flex",alignItems:"center",gap:w.xs,minWidth:"0"});const u=c.createElement("span");Object.assign(u.style,{fontWeight:"700",fontSize:x.sub,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}),e.appendChild(u);const y=c.createElement("span");Object.assign(y.style,{fontSize:x.body,lineHeight:k.none,color:E.label,flexShrink:"0",display:"none"}),e.appendChild(y);const $=4,d=c.createElement("span");Object.assign(d.style,{fontSize:x.body,color:E.label,whiteSpace:"nowrap",overflow:"hidden",flexShrink:"0",display:"none",marginLeft:w.sm}),e.appendChild(d);const l=c.createElement("div");l.dataset.wmapMetaPanel="1",Object.assign(l.style,{position:"absolute",top:"0",left:"0",right:"0",zIndex:M,background:E.menuBg,borderBottom:`1px solid ${E.menuBorder}`,boxShadow:L.menu,padding:`${w.md} ${w.lg}`,fontSize:x.body,display:"none"});function S(){return C(b).length>0||!!o}function O(){return o?[]:C(b).filter(([,n])=>!t.includes(n))}function z(){return e.clientWidth?u.scrollWidth>u.clientWidth||d.scrollWidth>d.clientWidth||e.scrollWidth>e.clientWidth:!0}function g(){u.textContent=t;const n=O();d.textContent=n.map(([p,h])=>`${A(p)}: ${h}`).join(" \xB7 "),d.style.display=n.length>0?"":"none",m=n.length>0&&n.length<=$&&!z(),d.style.display=m?"":"none";const f=S()&&!m;if(y.style.display=f?"":"none",e.style.cursor=f?"pointer":"",f||(i=!1),y.textContent=i?"\u25B4":"\u25BE",l.style.display=i?"block":"none",f?(e.setAttribute("aria-expanded",String(i)),e.setAttribute("aria-label",`Wafer info: ${t||"this wafer"}. ${i?"Click to collapse.":"Click to expand."}`)):(e.removeAttribute("aria-expanded"),e.removeAttribute("aria-label")),l.innerHTML="",i){if(o){const h=c.createElement("div");Object.assign(h.style,{fontWeight:"600",marginBottom:"3px"}),h.textContent=o.aggrMethod?`${o.lotSize} wafers stacked \xB7 ${o.aggrMethod}`:`${o.lotSize} wafers stacked`,l.appendChild(h)}const p=W(b);p&&l.appendChild(p)}}const I=j(e,n=>{!S()||m||(i=n,g())});g();const v=typeof ResizeObserver>"u"?null:new ResizeObserver(()=>g());return v?.observe(e),{wrap:e,metaPanel:l,update(n,f,p){t=n,b=f??{},o=p,g()},isEmpty(){return!t&&!S()},collapse(){i&&(i=!1,g())},destroy(){v?.disconnect(),I.destroy(),e.remove(),l.remove()}}}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
export { WMAP_VERSION, WMAP_BUILD_TIME } from './version.js';
|
|
1
2
|
export { toCanvas } from './toCanvas.js';
|
|
2
3
|
export type { ToCanvasOptions, ToCanvasResult, HitTarget, ViewportTransform } from './toCanvas.js';
|
|
3
4
|
export { renderWaferMap } from './renderWaferMap.js';
|
|
4
5
|
export type { RenderOptions, WaferMapController, WaferViewOptions, WaferPreferences, WaferDisplayState } from './renderWaferMap.js';
|
|
5
6
|
export { renderWaferGallery } from './renderWaferGallery.js';
|
|
6
7
|
export type { WaferMapDisplayItem, WaferMapDisplayItemFactory, GalleryOptions, GalleryController } from './renderWaferGallery.js';
|
|
7
|
-
export type { SummaryPanelOptions } from './summaryPanel.js';
|
|
8
|
+
export type { SummaryPanelOptions, FindingsNotice } from './summaryPanel.js';
|
|
8
9
|
export { buildDieListSection } from './dieList.js';
|
|
9
10
|
export type { DieListOptions, DieListDisplayOptions } from './dieList.js';
|
|
10
11
|
export { collectWarnings, severityOf } from './warnings.js';
|
|
@@ -1 +1 @@
|
|
|
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";export{openWaferMapGuide}from"./toolbar.js";export{ICONS}from"./icons.js";export{openReportModal}from"./toolbar.js";
|
|
1
|
+
export{WMAP_VERSION,WMAP_BUILD_TIME}from"./version.js";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";export{openWaferMapGuide}from"./toolbar.js";export{ICONS}from"./icons.js";export{openReportModal}from"./toolbar.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LotStatsSummary } from '../stats/types.js';
|
|
2
2
|
import type { WaferMapDisplayItem } from './renderWaferGallery.js';
|
|
3
|
+
import type { BinColors } from '../renderer/binColors.js';
|
|
3
4
|
import { type SaveImageHandler, type SaveTextHandler } from './toolbar.js';
|
|
4
5
|
export type InsightsView = 'overview' | 'distributions' | 'correlation';
|
|
5
6
|
/** Public option shape for `RenderOptions.insights`/`GalleryOptions.insights`. */
|
|
@@ -14,14 +15,29 @@ export interface InsightsOptions {
|
|
|
14
15
|
enabled?: boolean;
|
|
15
16
|
/** Which sub-tab is shown first. Default 'overview'. */
|
|
16
17
|
defaultView?: InsightsView;
|
|
18
|
+
/**
|
|
19
|
+
* Open the Insights view on mount instead of starting on the map/grid.
|
|
20
|
+
* Default false — the tab is offered, the map is what you see first.
|
|
21
|
+
*
|
|
22
|
+
* Set this for an analysis-first surface, where the charts are the point and
|
|
23
|
+
* the map is the secondary view. Symmetric with `summaryPanel.defaultOpen`.
|
|
24
|
+
* The chart suite is a lazily-imported chunk, so opening it on mount also
|
|
25
|
+
* pulls that chunk on load rather than on first click — worth knowing if the
|
|
26
|
+
* page is otherwise weight-sensitive.
|
|
27
|
+
*/
|
|
28
|
+
defaultOpen?: boolean;
|
|
17
29
|
}
|
|
18
30
|
export interface InsightsTabDeps {
|
|
19
31
|
/** Current gallery/single-wafer items — read fresh each render (a gallery's list can still be building). `null` entries (not-yet-built cards) are skipped. */
|
|
20
32
|
getItems: () => Array<WaferMapDisplayItem | null>;
|
|
21
33
|
/** Precomputed lot-level yield, when the host has one — reused directly instead of recomputing (see stats/yield.ts). Omit when there is no lot (e.g. a single wafer). */
|
|
22
34
|
getLotStats?: () => LotStatsSummary | undefined;
|
|
23
|
-
/**
|
|
24
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The host map's resolved bin colours (`View.binColors`, or the gallery's
|
|
37
|
+
* gallery-wide assignment) — read fresh each render so a live palette change is
|
|
38
|
+
* picked up, and so a bin is the same colour in a chart as on the map.
|
|
39
|
+
*/
|
|
40
|
+
getBinColors: () => BinColors;
|
|
25
41
|
passBins: number[];
|
|
26
42
|
/** Read fresh each render — used by the Overview tab's ring/quadrant regional yield cards. Default 4. */
|
|
27
43
|
getRingCount?: () => number;
|
|
@@ -37,6 +53,23 @@ export interface InsightsTabDeps {
|
|
|
37
53
|
* same test instead of hard-bin mode.
|
|
38
54
|
*/
|
|
39
55
|
openWafer?: (waferIndex: number, label: string, testNumber?: number) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Shows `testNumber` on the host's own map, in test-value mode, and leaves
|
|
58
|
+
* Insights — the single-wafer counterpart to `openWafer`.
|
|
59
|
+
*
|
|
60
|
+
* A single-wafer host (`renderWaferMap`) rightly passes no `openWafer`: the
|
|
61
|
+
* only wafer there is to open is the one already on screen, and opening it
|
|
62
|
+
* again in a modal would be two maps of one wafer. But a boxplot leaf click
|
|
63
|
+
* carries a second, independent payload — the selected test — and *that*
|
|
64
|
+
* half is as useful with one wafer as with twenty; without this the click was
|
|
65
|
+
* simply inert, and the only route from "this test looks wrong" to seeing it
|
|
66
|
+
* on the map was to leave Insights and find the test again in the toolbar's
|
|
67
|
+
* plot-mode picker.
|
|
68
|
+
*
|
|
69
|
+
* Ignored when `openWafer` is set (a gallery opens the wafer, which already
|
|
70
|
+
* lands on this test), and only consulted for a leaf row of the sole item.
|
|
71
|
+
*/
|
|
72
|
+
focusTest?: (testNumber: number) => void;
|
|
40
73
|
/** Default sub-tab shown on first render. Default 'overview'. */
|
|
41
74
|
defaultView?: InsightsView;
|
|
42
75
|
/**
|
|
@@ -49,13 +82,18 @@ export interface InsightsTabDeps {
|
|
|
49
82
|
label: string;
|
|
50
83
|
onBack: () => void;
|
|
51
84
|
};
|
|
85
|
+
/** Opens the user guide. Rendered as an icon at the end of the tab row —
|
|
86
|
+
* while Insights is showing, the map toolbar is hidden (it held only a
|
|
87
|
+
* back-to-gallery button, which the back tab already provides, and this
|
|
88
|
+
* help button), so this is where Help lives in this view. */
|
|
89
|
+
onOpenGuide?: () => void;
|
|
52
90
|
/**
|
|
53
91
|
* Show this tab's own identity strip (lot/wafer/product/etc.), mounted
|
|
54
92
|
* above the Overview/Distributions/Correlation tab bar so it stays in the
|
|
55
93
|
* same place across every sub-tab. Default true — needed by
|
|
56
|
-
* `renderWaferMap.ts`, whose single-wafer
|
|
57
|
-
*
|
|
58
|
-
*
|
|
94
|
+
* `renderWaferMap.ts`, whose single-wafer identity header is explicitly
|
|
95
|
+
* hidden while Insights is open (see `setInsightsOpen`, to avoid showing
|
|
96
|
+
* the same metadata twice), so this strip is its only identity display there.
|
|
59
97
|
* `renderWaferGallery.ts` sets this false: its own legend strip (built from
|
|
60
98
|
* the same wafer/lot metadata, via the same `buildMetadataStripRow`) stays
|
|
61
99
|
* mounted above the grid/Insights body in both views, so this strip would
|
|
@@ -63,6 +101,14 @@ export interface InsightsTabDeps {
|
|
|
63
101
|
* renderWaferGallery.ts's `rebuildLegend`/`setInsightsOpen`.
|
|
64
102
|
*/
|
|
65
103
|
showMetadataStrip?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Inset for this tab's own bands and content from the edge of the region it
|
|
106
|
+
* fills. Defaults to `EDGE_GUTTER`, which is right when the tab fills a
|
|
107
|
+
* top-level region (the gallery). `renderWaferMap` passes its own chrome
|
|
108
|
+
* inset instead, so the tab bar lines up with the identity row above it
|
|
109
|
+
* rather than sitting a gutter further in.
|
|
110
|
+
*/
|
|
111
|
+
contentInset?: string;
|
|
66
112
|
/** Document to build this tab's DOM into. Default `document` — pass the
|
|
67
113
|
* render's own `ownerDocument` when the container might live in a
|
|
68
114
|
* different document (e.g. a gallery card detached into its own popup
|