@wafertools/wafermap 0.30.2 → 0.30.4

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 (88) hide show
  1. package/AGENTS.md +16 -5
  2. package/CHANGELOG.md +432 -0
  3. package/README.md +6 -3
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +48 -0
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -0
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +4 -1
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +4 -1
  9. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +82 -8
  12. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  13. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  14. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  15. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  16. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/scatter.d.ts +5 -1
  18. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  19. package/dist/packages/canvas-adapter/charts/sweep.d.ts +31 -0
  20. package/dist/packages/canvas-adapter/charts/sweep.js +1 -0
  21. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  22. package/dist/packages/canvas-adapter/charts/trend.d.ts +3 -1
  23. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  24. package/dist/packages/canvas-adapter/dieList.js +2 -2
  25. package/dist/packages/canvas-adapter/drilldown.d.ts +23 -0
  26. package/dist/packages/canvas-adapter/drilldown.js +1 -0
  27. package/dist/packages/canvas-adapter/icons.js +1 -1
  28. package/dist/packages/canvas-adapter/insightsTab.d.ts +65 -3
  29. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  30. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  31. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  32. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  33. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  34. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  35. package/dist/packages/canvas-adapter/toolbar.d.ts +30 -2
  36. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  37. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  38. package/dist/packages/canvas-adapter/userGuideHtml.js +162 -37
  39. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  40. package/dist/packages/canvas-adapter/version.js +1 -1
  41. package/dist/packages/canvas-adapter/warnings.js +1 -1
  42. package/dist/packages/core/dies.d.ts +1 -1
  43. package/dist/packages/renderer/axisTicks.d.ts +32 -0
  44. package/dist/packages/renderer/axisTicks.js +1 -0
  45. package/dist/packages/renderer/binColors.js +1 -1
  46. package/dist/packages/renderer/buildView.d.ts +11 -37
  47. package/dist/packages/renderer/buildView.js +1 -1
  48. package/dist/packages/renderer/buildWaferMap.d.ts +97 -12
  49. package/dist/packages/renderer/buildWaferMap.js +1 -1
  50. package/dist/packages/renderer/deprecate.d.ts +13 -5
  51. package/dist/packages/renderer/deprecate.js +1 -1
  52. package/dist/packages/renderer/derivedTests/apply.d.ts +85 -0
  53. package/dist/packages/renderer/derivedTests/apply.js +1 -0
  54. package/dist/packages/renderer/derivedTests/evaluate.d.ts +48 -0
  55. package/dist/packages/renderer/derivedTests/evaluate.js +1 -0
  56. package/dist/packages/renderer/derivedTests/parser.d.ts +148 -0
  57. package/dist/packages/renderer/derivedTests/parser.js +2 -0
  58. package/dist/packages/renderer/fmt.d.ts +24 -2
  59. package/dist/packages/renderer/fmt.js +1 -1
  60. package/dist/packages/renderer/index.d.ts +1 -0
  61. package/dist/packages/renderer/index.js +1 -1
  62. package/dist/packages/renderer/spec.d.ts +66 -0
  63. package/dist/packages/renderer/spec.js +1 -0
  64. package/dist/packages/renderer/testLabel.d.ts +154 -0
  65. package/dist/packages/renderer/testLabel.js +1 -0
  66. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  67. package/dist/packages/stats/capability.d.ts +8 -2
  68. package/dist/packages/stats/capability.js +1 -1
  69. package/dist/packages/stats/correlation.d.ts +4 -1
  70. package/dist/packages/stats/correlation.js +1 -1
  71. package/dist/packages/stats/deprecated.d.ts +4 -1
  72. package/dist/packages/stats/deprecated.js +1 -1
  73. package/dist/packages/stats/index.d.ts +2 -2
  74. package/dist/packages/stats/index.js +1 -1
  75. package/dist/packages/stats/mergeTestDefs.d.ts +1 -1
  76. package/dist/packages/stats/mergeTestDefs.js +1 -1
  77. package/dist/packages/stats/renderFindingsReport.js +8 -28
  78. package/dist/packages/stats/renderSummaryReport.js +19 -28
  79. package/dist/packages/stats/reportHtml.d.ts +27 -0
  80. package/dist/packages/stats/reportHtml.js +35 -14
  81. package/dist/packages/stats/sweep.d.ts +186 -0
  82. package/dist/packages/stats/sweep.js +1 -0
  83. package/dist/packages/stats/sweepXFromName.d.ts +27 -0
  84. package/dist/packages/stats/sweepXFromName.js +1 -0
  85. package/dist/packages/stats/testPassRate.js +1 -1
  86. package/dist/packages/stats/types.d.ts +37 -4
  87. package/llms.txt +3 -2
  88. package/package.json +1 -1
@@ -1 +1 @@
1
- import{NO_DATA_FILL as ae}from"../../renderer/colorMap.js";import{maxOf as re,minOf as ce}from"../../core/utils.js";import{categorical as Me}from"./palette.js";import{buildScatterData as ve,buildScatterDataGrouped as Le}from"../../stats/scatter.js";import{pearsonOfPairs as ke}from"../../stats/correlation.js";import{buildFacetTable as Ce}from"../../stats/facets.js";import{SPACE as T,RADIUS as Ae,fontPx as j,FONT as q,CLR as H}from"../toolbar.js";import{cardShell as De,observeResize as Oe,makeTooltip as $e,attachChartTip as Be,makeTestSelect as Pe,makeWaferSelect as He,chartFillHeight as We,applyCanvasFlow as Ee,drawAxisUnit as Fe,resolveChartCanvasColors as Ie,makeAxisFormat as de,makeSeriesLegendItem as Ne,prepareCanvas as Re}from"./chartShell.js";const c=52,ze=16,s=16,_e=44;export function renderScatterPanel(W){const{title:me="Test scatter",items:C,testDefs:J,binColors:fe,onSaveImage:pe,groups:E}=W,{card:i,body:L,controlsRow:Q}=De(me,pe,W.ownerDocument),F=J.filter(t=>t.testNumber!==void 0),y=!!(E&&E.length>0),V=(E??[]).map(t=>t.key),ue=new Map(V.map((t,n)=>[t,n]));let p=W.xTestNumber??F[0]?.testNumber??null,x=W.yTestNumber??F[1]?.testNumber??p,A=null;const Z=()=>A!==null?C[A]?[C[A]]:[]:C;function ee(t,n,o){const l=i.ownerDocument.createElement("label");Object.assign(l.style,{display:"inline-flex",alignItems:"center",gap:T.xs,fontSize:q.body,color:H.label});const g=i.ownerDocument.createElement("span");g.textContent=t;const m=Pe(F,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:i.ownerDocument});return l.append(g,m),{wrap:l,select:m}}const{wrap:xe,select:he}=ee("X:",p,t=>{p=t,$()}),{wrap:ge,select:be}=ee("Y:",x,t=>{x=t,$()});Q.append(xe,ge),!y&&C.length>1&&Q.appendChild(He(C,A,t=>{A=t,$()},{ownerDocument:i.ownerDocument}));const D=i.ownerDocument.createElement("div");Object.assign(D.style,{color:H.warnText,background:H.warnBg,border:`1px solid ${H.warnBorder}`,borderRadius:Ae.control,padding:`${T.xs} ${T.md}`,fontSize:q.body,marginBottom:T.xs,display:"none"}),i.insertBefore(D,L);function te(){const t=y?[]:Ce(Z(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);t.length>0?(D.textContent=`\u26A0 Mixed ${t.join(", ")} across these points \u2014 trends here may be confounded (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,D.style.display=""):D.style.display="none"}te();const U=i.ownerDocument.createElement("div");Object.assign(U.style,{color:H.label,fontSize:q.body,marginBottom:T.xs}),i.insertBefore(U,L);function G(){const t=y?"One point per die \xB7 coloured by group \xB7 click legend to filter":"One point per die across all wafers \xB7 coloured by hard bin \xB7 click legend to filter",n=h.size===0?f:f.filter(g=>h.has(I(g))),{r:o,n:l}=ke(n);U.textContent=o===null?`${t} \xB7 n = ${l.toLocaleString()}`:`${t} \xB7 r = ${o.toFixed(3)} \xB7 n = ${l.toLocaleString()}`}const I=t=>y?t.group??"\u2014":String(t.hbin??0),ne=t=>y?Me(ue.get(t)??0):t==="0"?ae:fe?.get(Number(t))??ae,oe=t=>y?t:t==="0"?"No bin data":`Bin ${t}`,h=new Set,O=i.ownerDocument.createElement("div");Object.assign(O.style,{display:"flex",flexWrap:"wrap",gap:T.sm,marginTop:T.md,marginBottom:T.xl}),L.appendChild(O);const k=i.ownerDocument.createElement("canvas");k.style.display="block",k.style.cursor="crosshair",L.appendChild(k);const ye=$e(i);let f=[],w=0,N=1,M=0,R=1;function z(t){const n=J.find(o=>o.testNumber===t);return{unit:n?.unit,limitLow:n?.limitLow,limitHigh:n?.limitHigh}}function Se(){const t=L.clientWidth,n=Math.max(200,Math.min(400,t*.65)),o=We(i,L,k,n);return{w:t,h:o,plotW:Math.max(10,t-c-ze),plotH:Math.max(10,o-s-_e)}}const X=new Map;function ie(){for(const[t,n]of X){const o=h.has(t);n.setState({selected:o,dimmed:h.size>0&&!o})}}function le(t){O.innerHTML="",X.clear(),h.clear();for(const n of t){const o=Ne(oe(n),ne(n),i.ownerDocument),l=o.el;l.dataset.cat=n,X.set(n,o),Be(l,i,ye,`${oe(n)} \u2014 click to filter`),l.addEventListener("click",()=>{h.has(n)?h.delete(n):h.add(n),ie(),G(),_()}),O.appendChild(l)}ie()}function _(){Ee(k,O);const t=Ie(i),n=N-w,o=R-M,{w:l,h:g,plotW:m,plotH:d}=Se(),B=Re(k,i,l,g);if(!B)return;const{ctx:e}=B;if(f.length===0){e.font=`${j()}px system-ui, sans-serif`,e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.fillText("No data \u2014 select two parametric tests with values.",l/2,g/2);return}const b=4,Y=de(Math.max(Math.abs(w),Math.abs(N)),p!==null?z(p).unit:void 0),K=de(Math.max(Math.abs(M),Math.abs(R)),x!==null?z(x).unit:void 0);e.font=`${j(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=.5,e.fillStyle=t.textMuted;for(let a=0;a<=b;a++){const S=w+n*a/b,r=c+a/b*m;e.beginPath(),e.moveTo(r,s),e.lineTo(r,s+d),e.stroke(),e.textAlign="center",e.textBaseline="top",e.fillText(Y.tick(S),r,s+d+4);const v=M+o*a/b,u=s+(1-a/b)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="right",e.textBaseline="middle",e.fillText(K.tick(v),c-4,u)}Y.unitLabel&&Fe(e,Y.unitLabel,c+m/2,s+d+24,t.textMuted),K.unitLabel&&(e.save(),e.font=`${j(-1)}px system-ui, sans-serif`,e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.translate(6,s+d/2),e.rotate(-Math.PI/2),e.fillText(`(${K.unitLabel})`,0,0),e.restore());const P=h.size===0?f:f.filter(a=>h.has(I(a))),se=P.length>5e3?Math.ceil(P.length/5e3):1;e.globalAlpha=Math.max(.15,Math.min(.7,200/(P.length/se)));for(let a=0;a<P.length;a+=se){const S=P[a],r=c+(S.x-w)/n*m,v=s+(1-(S.y-M)/o)*d;e.beginPath(),e.arc(r,v,2.5,0,Math.PI*2),e.fillStyle=ne(I(S)),e.fill()}if(e.globalAlpha=1,p!==null&&x!==null){const a=z(p),S=z(x);e.strokeStyle=t.textMuted,e.fillStyle=t.textMuted,e.lineWidth=1,e.setLineDash([4,3]),e.font=`${j(-1)}px system-ui, sans-serif`,e.textBaseline="top";for(const[r,v]of[[a.limitLow,"LSL"],[a.limitHigh,"USL"]]){if(r===void 0||r<w||r>N)continue;const u=c+(r-w)/n*m;e.beginPath(),e.moveTo(u,s),e.lineTo(u,s+d),e.stroke(),e.textAlign="left",e.fillText(v,u+2,s+2)}for(const[r,v]of[[S.limitLow,"LSL"],[S.limitHigh,"USL"]]){if(r===void 0||r<M||r>R)continue;const u=s+(1-(r-M)/o)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="left",e.fillText(v,c+3,u+2)}e.setLineDash([])}e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,s),e.lineTo(c,s+d),e.moveTo(c,s+d),e.lineTo(c+m,s+d),e.stroke()}function $(){if(te(),F.length<2||p===null||x===null){f=[],le([]),G(),_();return}f=y?Le(E,p,x):ve(Z(),p,x);let t;if(y){const n=new Set(f.map(I));t=V.filter(o=>n.has(o))}else t=Array.from(new Set(f.map(n=>n.hbin??0))).sort((n,o)=>n-o).map(String);if(le(t),G(),f.length>0){const n=f.map(b=>b.x),o=f.map(b=>b.y),l=ce(n),g=re(n),m=ce(o),d=re(o),B=(g-l)*.05||1,e=(d-m)*.05||1;w=l-B,N=g+B,M=m-e,R=d+e}_()}const Te=Oe(i,()=>_());$();function we(t,n){p=t,x=n,he.value=String(t),be.value=String(n),$()}return{card:i,setXY:we,destroy:()=>Te.disconnect()}}
1
+ import{NO_DATA_FILL as mt}from"../../renderer/colorMap.js";import{maxOf as ut,minOf as pt}from"../../core/utils.js";import{categorical as Ct}from"./palette.js";import{buildScatterData as Lt,buildScatterDataGrouped as At}from"../../stats/scatter.js";import{pearsonOfPairs as Ot}from"../../stats/correlation.js";import{buildFacetTable as Dt}from"../../stats/facets.js";import{SPACE as M,RADIUS as Bt,fontPx as R,FONT as Z,CLR as _}from"../toolbar.js";import{fitTicks as xt}from"../../renderer/axisTicks.js";import{cardShell as Pt,observeResize as $t,makeTooltip as Ht,attachChartTip as Et,makeTestSelect as It,makeWaferSelect as Wt,chartFillHeight as Nt,applyCanvasFlow as Ft,drawAxisUnit as Rt,resolveChartCanvasColors as _t,makeAxisFormat as ht,horizontalTickSpacing as zt,VERTICAL_TICK_SPACING_PX as Gt,makeSeriesLegendItem as jt,prepareCanvas as Ut}from"./chartShell.js";const r=52,Xt=16,l=16,Yt=44;export const NO_BIN_CATEGORY="none";export function binCategoryOf(b){return b===void 0?NO_BIN_CATEGORY:String(b)}export function binCategories(b){const z=new Set;let T=!1;for(const v of b)v.hbin===void 0?T=!0:z.add(v.hbin);const O=Array.from(z).sort((v,Y)=>v-Y).map(String);return T&&O.push(NO_BIN_CATEGORY),O}export function renderScatterPanel(b){const{title:z="Test scatter",items:T,testDefs:O,binColors:v,onSaveImage:Y,groups:G}=b,{card:i,body:C,controlsRow:tt}=Pt(z,Y,b.ownerDocument),j=O.filter(e=>e.testNumber!==void 0),S=!!(G&&G.length>0),et=(G??[]).map(e=>e.key),gt=new Map(et.map((e,n)=>[e,n]));let m=b.xTestNumber??j[0]?.testNumber??null,x=b.yTestNumber??j[1]?.testNumber??m,D=null;const nt=()=>D!==null?T[D]?[T[D]]:[]:T;function ot(e,n,o){const s=i.ownerDocument.createElement("label");Object.assign(s.style,{display:"inline-flex",alignItems:"center",gap:M.xs,fontSize:Z.body,color:_.label});const g=i.ownerDocument.createElement("span");g.textContent=e;const c=It(j,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:i.ownerDocument});return s.append(g,c),{wrap:s,select:c}}const{wrap:bt,select:yt}=ot("X:",m,e=>{m=e,E()}),{wrap:Tt,select:St}=ot("Y:",x,e=>{x=e,E()});tt.append(bt,Tt),!S&&T.length>1&&tt.appendChild(Wt(T,D,e=>{D=e,E()},{ownerDocument:i.ownerDocument}));const B=i.ownerDocument.createElement("div");Object.assign(B.style,{color:_.warnText,background:_.warnBg,border:`1px solid ${_.warnBorder}`,borderRadius:Bt.control,padding:`${M.xs} ${M.md}`,fontSize:Z.body,marginBottom:M.xs,display:"none"}),i.insertBefore(B,C);function it(){const e=S?[]:Dt(nt(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);e.length>0?(B.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.`,B.style.display=""):B.style.display="none"}it();const K=i.ownerDocument.createElement("div");Object.assign(K.style,{color:_.label,fontSize:Z.body,marginBottom:M.xs}),i.insertBefore(K,C);function V(){const e=S?"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?u:u.filter(g=>h.has(U(g))),{r:o,n:s}=Ot(n);K.textContent=o===null?`${e} \xB7 n = ${s.toLocaleString()}`:`${e} \xB7 r = ${o.toFixed(3)} \xB7 n = ${s.toLocaleString()}`}const U=e=>S?e.group??"\u2014":binCategoryOf(e.hbin),st=e=>S?Ct(gt.get(e)??0):e===NO_BIN_CATEGORY?mt:v?.get(Number(e))??mt,lt=e=>S?e:e===NO_BIN_CATEGORY?"No bin data":`Bin ${e}`,h=new Set,P=i.ownerDocument.createElement("div");Object.assign(P.style,{display:"flex",flexWrap:"wrap",gap:M.sm,marginTop:M.md,marginBottom:M.xl}),C.appendChild(P);const L=i.ownerDocument.createElement("canvas");L.style.display="block",L.style.cursor="crosshair",C.appendChild(L);const wt=Ht(i);let u=[],y=0,A=1,w=0,$=1;function H(e){const n=O.find(o=>o.testNumber===e);return{unit:n?.unit,limitLow:n?.limitLow,limitHigh:n?.limitHigh}}function Mt(){const e=C.clientWidth,n=Math.max(200,Math.min(400,e*.65)),o=Nt(i,C,L,n);return{w:e,h:o,plotW:Math.max(10,e-r-Xt),plotH:Math.max(10,o-l-Yt)}}const q=new Map;function at(){for(const[e,n]of q){const o=h.has(e);n.setState({selected:o,dimmed:h.size>0&&!o})}}function rt(e){P.innerHTML="",q.clear(),h.clear();for(const n of e){const o=jt(lt(n),st(n),i.ownerDocument),s=o.el;s.dataset.cat=n,q.set(n,o),Et(s,i,wt,`${lt(n)} \u2014 click to filter`),s.addEventListener("click",()=>{h.has(n)?h.delete(n):h.add(n),at(),V(),X()}),P.appendChild(s)}at()}function X(){Ft(L,P);const e=_t(i),n=A-y,o=$-w,{w:s,h:g,plotW:c,plotH:a}=Mt(),I=Ut(L,i,s,g);if(!I)return;const{ctx:t}=I;if(u.length===0){t.font=`${R()}px 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.",s/2,g/2);return}t.font=`${R(-1)}px system-ui, sans-serif`;const W=m!==null?H(m).unit:void 0,ct=xt(y,A,c,zt(t,Math.max(Math.abs(y),Math.abs(A)),W)),dt=xt(w,$,a,()=>Gt),J=ht(Math.max(Math.abs(y),Math.abs(A)),m!==null?H(m).unit:void 0,ct.step||void 0),Q=ht(Math.max(Math.abs(w),Math.abs($)),x!==null?H(x).unit:void 0,dt.step||void 0);t.font=`${R(-1)}px system-ui, sans-serif`,t.strokeStyle=e.border,t.lineWidth=.5,t.fillStyle=e.textMuted;for(const d of ct.ticks){const f=r+(d-y)/n*c;t.beginPath(),t.moveTo(f,l),t.lineTo(f,l+a),t.stroke(),t.textAlign="center",t.textBaseline="top",t.fillText(J.tick(d),f,l+a+4)}for(const d of dt.ticks){const f=l+(1-(d-w)/o)*a;t.beginPath(),t.moveTo(r,f),t.lineTo(r+c,f),t.stroke(),t.textAlign="right",t.textBaseline="middle",t.fillText(Q.tick(d),r-4,f)}J.unitLabel&&Rt(t,J.unitLabel,r+c/2,l+a+24,e.textMuted),Q.unitLabel&&(t.save(),t.font=`${R(-1)}px system-ui, sans-serif`,t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="middle",t.translate(6,l+a/2),t.rotate(-Math.PI/2),t.fillText(`(${Q.unitLabel})`,0,0),t.restore());const N=h.size===0?u:u.filter(d=>h.has(U(d))),ft=N.length>5e3?Math.ceil(N.length/5e3):1;t.globalAlpha=Math.max(.15,Math.min(.7,200/(N.length/ft)));for(let d=0;d<N.length;d+=ft){const f=N[d],p=r+(f.x-y)/n*c,F=l+(1-(f.y-w)/o)*a;t.beginPath(),t.arc(p,F,2.5,0,Math.PI*2),t.fillStyle=st(U(f)),t.fill()}if(t.globalAlpha=1,m!==null&&x!==null){const d=H(m),f=H(x);t.strokeStyle=e.textMuted,t.fillStyle=e.textMuted,t.lineWidth=1,t.setLineDash([4,3]),t.font=`${R(-1)}px system-ui, sans-serif`,t.textBaseline="top";for(const[p,F]of[[d.limitLow,"Lo limit"],[d.limitHigh,"Hi limit"]]){if(p===void 0||p<y||p>A)continue;const k=r+(p-y)/n*c;t.beginPath(),t.moveTo(k,l),t.lineTo(k,l+a),t.stroke(),t.textAlign="left",t.fillText(F,k+2,l+2)}for(const[p,F]of[[f.limitLow,"Lo limit"],[f.limitHigh,"Hi limit"]]){if(p===void 0||p<w||p>$)continue;const k=l+(1-(p-w)/o)*a;t.beginPath(),t.moveTo(r,k),t.lineTo(r+c,k),t.stroke(),t.textAlign="left",t.fillText(F,r+3,k+2)}t.setLineDash([])}t.strokeStyle=e.border,t.lineWidth=1,t.beginPath(),t.moveTo(r,l),t.lineTo(r,l+a),t.moveTo(r,l+a),t.lineTo(r+c,l+a),t.stroke()}function E(){if(it(),j.length<2||m===null||x===null){u=[],rt([]),V(),X();return}u=S?At(G,m,x):Lt(nt(),m,x);let e;if(S){const n=new Set(u.map(U));e=et.filter(o=>n.has(o))}else e=binCategories(u);if(rt(e),V(),u.length>0){const n=u.map(W=>W.x),o=u.map(W=>W.y),s=pt(n),g=ut(n),c=pt(o),a=ut(o),I=(g-s)*.05||1,t=(a-c)*.05||1;y=s-I,A=g+I,w=c-t,$=a+t}X()}const kt=$t(i,()=>X());E();function vt(e,n){m=e,x=n,yt.value=String(e),St.value=String(n),E()}return{card:i,setXY:vt,destroy:()=>kt.disconnect()}}
@@ -0,0 +1,31 @@
1
+ import { type SweepSpec } from '../../stats/sweep.js';
2
+ import type { Die } from '../../core/dies.js';
3
+ import type { TestDef } from '../../renderer/buildWaferMap.js';
4
+ import { type SaveImageHandler } from './chartShell.js';
5
+ export interface SweepPanelOptions {
6
+ /** The curve definition. Title comes from here, not from the panel. */
7
+ spec: SweepSpec;
8
+ /** Population to aggregate over — already scoped by the caller. Scope is not
9
+ * a property of the sweep: the same definition is valid for any population,
10
+ * which is what keeps a saved spec portable. */
11
+ dies: Die[];
12
+ /**
13
+ * Who `dies` are, read straight after the count — `"selected on W03"` gives
14
+ * "median across 12 dies selected on W03". Omit when the dies are simply the
15
+ * view's own population (the Insights tab, which states its scope itself).
16
+ * A sweep of a hand-picked subset looks exactly like a whole-wafer one, so
17
+ * a drilldown must always pass this.
18
+ */
19
+ population?: string;
20
+ testDefs: TestDef[] | undefined;
21
+ onSaveImage?: SaveImageHandler;
22
+ ownerDocument?: Document;
23
+ }
24
+ export interface SweepPanelHandle {
25
+ card: HTMLElement;
26
+ destroy: () => void;
27
+ /** Re-aggregate over a new population, keeping the same curve definition. */
28
+ setDies: (dies: Die[]) => void;
29
+ }
30
+ export declare function renderSweepPanel(options: SweepPanelOptions): SweepPanelHandle;
31
+ //# sourceMappingURL=sweep.d.ts.map
@@ -0,0 +1 @@
1
+ import{buildSweepData as ce,sweepAppliesTo as fe}from"../../stats/sweep.js";import{fmt as C}from"../../renderer/fmt.js";import{SPACE as ne,fontPx as pe,FONT as oe,CLR as G,ALPHA as de}from"../toolbar.js";import{categorical as he}from"./palette.js";import{cardShell as me,observeResize as ue,makeTooltip as xe,positionChartTooltip as ge,renderEmptyState as se,chartFillHeight as be,populationPhrase as ye,PADDING as Y,prepareCanvas as $e,makeAxisFormat as Te,VERTICAL_TICK_SPACING_PX as ve}from"./chartShell.js";import{fitTicks as Se}from"../../renderer/axisTicks.js";import{escHtml as m}from"../../core/utils.js";import{unmarkedLabel as Me,derivedTestNote as Le,DERIVED_MARK as J,DERIVED_KEY as we}from"../../renderer/testLabel.js";function _(u,a){return a?C(u,a).replace(/(\.\d*?)0+(?=\D|$)/,"$1").replace(/\.(?=\D|$)/,""):Number.isInteger(u)?String(u):String(Number(u.toPrecision(3)))}function Pe(u){const a=Me(u.label),S=a.length>9?`${a.slice(0,8)}\u2026`:a;return u.derived?`${J} ${S}`:S}const ie=260,ke=62,Ae=56,Ee=3;function Ie(u,a){const S=u.series[0]?.points[0]?.testNumber;return S===void 0?void 0:a?.find(j=>j.testNumber===S)?.unit}export function renderSweepPanel(u){const{spec:a,testDefs:S,onSaveImage:j,population:re}=u;let z=u.dies;const{card:y,body:M}=me(a.title,j,u.ownerDocument);y.style.alignSelf="start";const V=y.ownerDocument,N=V.createElement("div");Object.assign(N.style,{color:G.label,fontSize:oe.body,marginBottom:ne.sm}),y.insertBefore(N,M);const W=V.createElement("div");Object.assign(W.style,{color:G.label,fontSize:oe.body,marginTop:ne.sm}),y.appendChild(W);const L=xe(y);let X=null,E=null,$;const H=n=>a.series[n]?.color??he(n);function Q(){if(M.innerHTML="",W.innerHTML="",X?.disconnect(),X=null,a.series.length===0){N.textContent="",se(M,"This sweep defines no series.");return}if(E=ce(z,S,a),$=Ie(E,S),E.series.filter(i=>i.points.some(T=>T.count>0)).length===0){N.textContent="",se(M,fe(a,S)?"No values recorded for the tests in this sweep.":"None of this sweep\u2019s tests are in this data \u2014 it may belong to another test program."),Z();return}N.textContent=`Line = median across ${ye(E.dieCount,z.length,re)}, band = p10\u2013p90`+(a.series.length>2?" \xB7 crossing and separation measured between the first two series":"");const c=V.createElement("canvas");c.style.display="block",M.appendChild(c);let o=null;const w=()=>{const i=E;if(i===null)return;const T=Math.max(1,M.clientWidth),B=Math.max(ie,be(y,M,c,ie)),F=$e(c,y,T,B);if(!F)return;const{ctx:e,theme:x}=F,g=t=>i.xScale==="log"?Math.log10(t):t;let f=1/0,h=-1/0,b=1/0,v=-1/0;for(const t of i.series)for(const s of t.points)s.count!==0&&(f=Math.min(f,s.p10),h=Math.max(h,s.p90),b=Math.min(b,g(s.x)),v=Math.max(v,g(s.x)));for(const t of i.separations)f=Math.min(f,t.y),h=Math.max(h,t.y);if(!Number.isFinite(f)||!Number.isFinite(h))return;f===h&&(f-=1,h+=1);const l=(h-f)*.08;if(f-=l,h+=l,b===v&&(b-=.5,v+=.5),i.xScale==="log"){const t=(v-b)*.03;b-=t,v+=t}e.font=`${pe(-1)}px system-ui, sans-serif`;const r=Y+ke,p=T-Y-8,R=Y,P=B-Ae,k=t=>P-(t-f)/(h-f)*(P-R),D=t=>r+(g(t)-b)/(v-b)*(p-r);o={left:r,right:p,xLo:b,xHi:v,yOf:k,ax:g},e.strokeStyle=x.border,e.lineWidth=1,e.beginPath(),e.moveTo(r,R),e.lineTo(r,P),e.lineTo(p,P),e.stroke(),e.fillStyle=x.text,e.textAlign="right",e.textBaseline="middle";const q=Se(f,h,P-R,()=>ve),le=Te(Math.max(Math.abs(f),Math.abs(h)),$,q.step||void 0);for(const t of q.ticks)e.fillText(le.tickWithUnit(t),r-6,k(t));for(const t of i.separations){const s=k(t.y);e.save(),e.strokeStyle=x.limitLine,e.globalAlpha=.55,e.setLineDash([4,3]),e.beginPath(),e.moveTo(r,s),e.lineTo(p,s),e.stroke(),e.restore()}if(i.series.forEach((t,s)=>{const A=t.points.filter(d=>d.count>0);if(!(A.length<2)){e.save(),e.fillStyle=H(s),e.globalAlpha=.14,e.beginPath(),A.forEach((d,I)=>{const O=D(d.x),te=k(d.p90);I===0?e.moveTo(O,te):e.lineTo(O,te)});for(let d=A.length-1;d>=0;d--){const I=A[d];e.lineTo(D(I.x),k(I.p10))}e.closePath(),e.fill(),e.restore()}}),i.series.forEach((t,s)=>{e.strokeStyle=H(s),e.lineWidth=1.75,e.beginPath();let A=!1;for(const d of t.points){if(d.count===0){A=!1;continue}const I=D(d.x),O=k(d.median);A?e.lineTo(I,O):e.moveTo(I,O),A=!0}e.stroke(),e.fillStyle=H(s);for(const d of t.points)d.count!==0&&(e.beginPath(),e.arc(D(d.x),k(d.median),Ee,0,Math.PI*2),e.fill())}),i.crossing){const t=D(i.crossing.x),s=k(i.crossing.y);e.save(),e.strokeStyle=x.text,e.globalAlpha=.45,e.setLineDash([3,3]),e.beginPath(),e.moveTo(t,s),e.lineTo(t,P),e.stroke(),e.restore(),e.save(),e.strokeStyle=x.text,e.lineWidth=2,e.beginPath(),e.arc(t,s,5.5,0,Math.PI*2),e.stroke(),e.restore()}e.fillStyle=x.text,e.textBaseline="top",e.textAlign="center";const K=i.xIsPhysical?[...new Set(i.series.flatMap(t=>t.points.map(s=>s.x)))].sort((t,s)=>t-s).map(t=>({x:t,label:_(t,i.xUnit)})):(i.series[0]?.points??[]).map(t=>({x:t.x,label:Pe(t)})),ae=Math.max(1,Math.ceil(K.length*46/Math.max(1,p-r)));K.forEach((t,s)=>{s%ae!==0&&s!==K.length-1||e.fillText(t.label,D(t.x),P+6)}),e.save(),e.fillStyle=x.text,e.globalAlpha=.75,e.textAlign="center",e.textBaseline="bottom",e.fillText(i.xLabel,(r+p)/2,B-4),e.restore(),e.save(),e.fillStyle=x.text,e.globalAlpha=.75,e.translate(12,(R+P)/2),e.rotate(-Math.PI/2),e.textBaseline="top",e.fillText(i.yLabel+($?` (${$})`:""),0,0),e.restore(),e.textAlign="left",e.textBaseline="middle";let U=r+4;const ee=R+8;i.series.forEach((t,s)=>{e.fillStyle=H(s),e.fillRect(U,ee-4,10,3),U+=14,e.fillStyle=x.text,e.fillText(t.label,U,ee),U+=e.measureText(t.label).width+16})};w(),X=ue(M,w),c.addEventListener("mousemove",i=>{const T=E;if(T===null||o===null){L.style.display="none";return}const B=c.getBoundingClientRect(),F=i.clientX-B.left,e=o.xLo+(F-o.left)/Math.max(1,o.right-o.left)*(o.xHi-o.xLo),x=o.ax;let g=null;for(const l of T.series)for(const r of l.points){if(r.count===0)continue;const p=Math.abs(x(r.x)-e);(g===null||p<g.dist)&&(g={x:r.x,dist:p})}if(g===null){L.style.display="none";return}const f=T.series.map(l=>l.points.find(r=>r.x===g.x&&r.count>0)),h=T.series.map((l,r)=>{const p=f[r];return p===void 0?"":`<span style="color:${H(r)}">\u25A0</span> ${p.derived?`${J} `:""}${m(l.label)}: <strong>${m(C(p.median,$))}</strong><br><span style="opacity:${de.muted}">p10 ${m(C(p.p10,$))} \u2013 p90 ${m(C(p.p90,$))} \xB7 n=${p.count.toLocaleString()}</span>`}).filter(Boolean);if(h.length===0){L.style.display="none";return}const b=T.series.flatMap(l=>l.points).find(l=>l.x===g.x&&l.count>0),v=[...new Set(f.map(l=>Le(l)).filter(l=>l!==void 0))];L.innerHTML=`<strong>${m(b?.label??"")}</strong><br>`+h.join("<br>")+v.map(l=>`<br><em>${m(l)}</em>`).join(""),L.style.display="block",ge(L,y,i.clientX,i.clientY)}),c.addEventListener("mouseleave",()=>{L.style.display="none"}),Z()}function Z(){const n=E;if(n===null)return;const c=[];n.notMeasured!==void 0?c.push(`<strong>Crossing and widths:</strong> not measured \u2014 ${m(n.notMeasured)}`):n.crossing?c.push(`<strong>Crossing:</strong> ${m(`${n.xLabel} = ${_(n.crossing.x,n.xUnit)}`)} \xB7 ${m(`${n.yLabel} ${C(n.crossing.y,$)}`)}`+(n.crossing.multiple?" <em>(first of several \u2014 the curves cross more than once)</em>":"")):(a.crossing??!0)&&a.series.length>=2&&c.push("<strong>Crossing:</strong> the first two series do not cross over the swept range");for(const o of n.separations)if(o.distance!==null){const w=o.ratio!==void 0&&o.from!==void 0&&o.to!==void 0?`\xD7${Number(o.ratio.toPrecision(3))} on the ${n.xLabel} axis (${_(o.from,n.xUnit)} \u2192 ${_(o.to,n.xUnit)})`:`${_(o.distance,n.xUnit)} on the ${n.xLabel} axis`;c.push(`<strong>Width at ${m(C(o.y,$))}:</strong> ${m(w)}`)}else{const w=o.reason==="second-series-never-reaches"?n.series[1]?.label:n.series[0]?.label;c.push(`<strong>Width at ${m(C(o.y,$))}:</strong> not measurable \u2014 ${m(w??"a series")} never reaches this level`)}n.series.some(o=>o.points.some(w=>w.derived))&&c.push(`${J} ${m(we)} \u2014 hover a point for the expression`);for(const o of n.warnings)c.push(`<span style="color:${G.errText}">${m(o)}</span>`);W.innerHTML=c.join("<br>")}return Q(),{card:y,destroy:()=>{X?.disconnect(),L.remove(),y.remove()},setDies:n=>{z=n,Q()}}}
@@ -1 +1 @@
1
- import{buildTestPassRateData as F,hasJudgeableTests as B}from"../../stats/testPassRate.js";import{categorical as I,QUANTITY as N}from"./palette.js";import{SPACE as H,FONT as _,CLR as w}from"../toolbar.js";import{cardShell as A,makeTooltip as O,makeSegmented as j,renderEmptyState as y}from"./chartShell.js";import{renderGroupedBarPlot as M}from"./groupedBarPlot.js";import{escHtml as g}from"../../core/utils.js";const k=100,G=8,U={spec:"Spec limits",testFlag:"Tester flag",functional:"Functional"},K={spec:"Parametric pass rate \xB7 spec limits",testFlag:"Parametric pass rate \xB7 tester flag",functional:"Functional test pass rate"};export function renderTestPassRatePanel(f){const{groups:u,testDefs:b,onSaveImage:$}=f,p=f.ownerDocument??document,P=u.flatMap(e=>e.items.flatMap(a=>a.dies??[])),d=["spec","testFlag","functional"].filter(e=>B(b,e,P));let s=d[0]??"spec";const h=e=>K[e],{card:o,heading:v,body:i,controlsRow:x}=A(h(s),$,p);o.style.alignSelf="start",d.length>1&&x.appendChild(j(d.map(e=>[e,U[e]]),s,e=>{s=e,v.textContent=h(s),L()},p));const n=p.createElement("div");Object.assign(n.style,{color:w.label,fontSize:_.body,marginBottom:H.sm}),o.insertBefore(n,i);const D=O(o);let m=null;function L(){if(m?.destroy(),m=null,i.innerHTML="",d.length===0){n.textContent="",y(i,"No tests with a pass/fail verdict \u2014 parametric tests need spec limits, functional tests need recorded results.");return}const e=F(u,b,s);if(e.rows.length===0){n.textContent="",y(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,R=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=R+(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:w.warnText}),n.appendChild(t)}const S=e.rows,C=t=>a?I(t):N;m=M(o,i,D,{rows:S.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:k,maxVisibleRows:G,showLegend:a,maxLabelChars:14,emptyBarText:"no data",colorOf:C,tooltipHtml:(t,l)=>{const r=S[t],c=r.byGroup[l],T=a?` \xB7 ${g(String(e.groups[l]))}`:"";if(c.passRatePercent===null)return`<strong>${g(r.label)}</strong>${T}<br>no dies with a verdict`;const E=s==="spec"&&!a&&(r.failLowDies||r.failHighDies)?`<br>${r.failLowDies??0} below LSL \xB7 ${r.failHighDies??0} above USL`:"";return`<strong>${g(r.label)}</strong>${T}<br>${c.passRatePercent.toFixed(1)}% pass<br>${c.passDies.toLocaleString()} pass \xB7 ${c.failDies.toLocaleString()} fail \xB7 n = ${c.totalDies.toLocaleString()}`+E}})}return L(),{card:o,destroy:()=>{m?.destroy(),D.remove(),o.remove()}}}
1
+ import{buildTestPassRateData as F,hasJudgeableTests as B}from"../../stats/testPassRate.js";import{categorical as I,QUANTITY as N}from"./palette.js";import{SPACE as H,FONT as _,CLR as S}from"../toolbar.js";import{cardShell as A,makeTooltip as O,makeSegmented as j,renderEmptyState as y}from"./chartShell.js";import{renderGroupedBarPlot as M}from"./groupedBarPlot.js";import{escHtml as g}from"../../core/utils.js";const k=100,G=8,K={spec:"Test limits",testFlag:"Tester flag",functional:"Functional"},U={spec:"Parametric pass rate \xB7 test limits",testFlag:"Parametric pass rate \xB7 tester flag",functional:"Functional test pass rate"};export function renderTestPassRatePanel(f){const{groups:u,testDefs:b,onSaveImage:$}=f,p=f.ownerDocument??document,P=u.flatMap(t=>t.items.flatMap(a=>a.dies??[])),d=["spec","testFlag","functional"].filter(t=>B(b,t,P));let s=d[0]??"spec";const h=t=>U[t],{card:o,heading:v,body:i,controlsRow:x}=A(h(s),$,p);o.style.alignSelf="start",d.length>1&&x.appendChild(j(d.map(t=>[t,K[t]]),s,t=>{s=t,v.textContent=h(s),T()},p));const n=p.createElement("div");Object.assign(n.style,{color:S.label,fontSize:_.body,marginBottom:H.sm}),o.insertBefore(n,i);const D=O(o);let m=null;function T(){if(m?.destroy(),m=null,i.innerHTML="",d.length===0){n.textContent="",y(i,"No tests with a pass/fail verdict \u2014 parametric tests need test limits, functional tests need recorded results.");return}const t=F(u,b,s);if(t.rows.length===0){n.textContent="",y(i,s==="spec"?"No parametric test has test limits to judge against.":s==="testFlag"?"No parametric test has a recorded tester verdict.":"No functional test has recorded results.");return}const a=t.groups.length>1,R=s==="spec"?"Dies within test limits, per test":s==="testFlag"?"Dies the tester marked pass, per test":"Dies passing the recorded verdict, per test";if(n.textContent=R+(a?" \xB7 one bar per group":"")+" \xB7 worst first",t.disagreementDies){const e=p.createElement("span");e.textContent=` \xB7 ${t.disagreementDies.toLocaleString()} die${t.disagreementDies===1?"":"s"} judged differently by limits and tester flag`,Object.assign(e.style,{color:S.warnText}),n.appendChild(e)}const L=t.rows,C=e=>a?I(e):N;m=M(o,i,D,{rows:L.map(e=>({label:e.label,bars:e.byGroup.map(l=>({fraction:l.passRatePercent===null?null:l.passRatePercent/100})),trailing:e.overall.passRatePercent===null?"\u2014":`${e.overall.passRatePercent.toFixed(1)}%`})),groups:t.groups,labelWidth:k,maxVisibleRows:G,showLegend:a,maxLabelChars:14,emptyBarText:"no data",colorOf:C,tooltipHtml:(e,l)=>{const r=L[e],c=r.byGroup[l],w=a?` \xB7 ${g(String(t.groups[l]))}`:"";if(c.passRatePercent===null)return`<strong>${g(r.label)}</strong>${w}<br>no dies with a verdict`;const E=s==="spec"&&!a&&(r.failLowDies||r.failHighDies)?`<br>${r.failLowDies??0} below the low limit \xB7 ${r.failHighDies??0} above the high limit`:"";return`<strong>${g(r.label)}</strong>${w}<br>${c.passRatePercent.toFixed(1)}% pass<br>${c.passDies.toLocaleString()} pass \xB7 ${c.failDies.toLocaleString()} fail \xB7 n = ${c.totalDies.toLocaleString()}`+E}})}return T(),{card:o,destroy:()=>{m?.destroy(),D.remove(),o.remove()}}}
@@ -1,6 +1,6 @@
1
1
  import { type TrendItem } from '../../stats/trend.js';
2
2
  import { type TestDef } from '../../renderer/buildWaferMap.js';
3
- import { type AxisPrefs, type SaveImageHandler } from './chartShell.js';
3
+ import { type AxisPrefs, type SaveImageHandler, type WaferContextMenuHandler } from './chartShell.js';
4
4
  export interface TrendPanelOptions {
5
5
  title?: string;
6
6
  items: TrendItem[];
@@ -9,6 +9,8 @@ export interface TrendPanelOptions {
9
9
  onSaveImage?: SaveImageHandler;
10
10
  /** Click a point to open that wafer's map on this test. */
11
11
  onOpen?: (key: number, testNumber: number) => void;
12
+ /** Right-click on a wafer's point — see `WaferContextMenuHandler`. */
13
+ onWaferContextMenu?: WaferContextMenuHandler;
12
14
  /**
13
15
  * What the dashed reference line is, as a noun phrase — "lot mean" when the
14
16
  * items are one lot, else "mean of all wafers" (see stats/population.ts).
@@ -1 +1 @@
1
- import{buildTestTrendData as fe,trendCentre as ue}from"../../stats/trend.js";import{isParametricTest as he}from"../../renderer/buildWaferMap.js";import{fmt as D}from"../../renderer/fmt.js";import{SPACE as oe,fontPx as pe,FONT as ge,CLR as xe}from"../toolbar.js";import{QUANTITY as _}from"./palette.js";import{cardShell as be,observeResize as ye,makeTooltip as ve,positionChartTooltip as Te,makeLinkedTestSelect as Le,makeLinkedAxisPrefs as we,renderEmptyState as z,growCardToFitContent as ke,chartFillHeight as Ce,PADDING as X,resolveAxisRange as Se,shouldIncludeLimitsByDefault as Ae,drawOffAxisLimits as Pe,prepareCanvas as Ne}from"./chartShell.js";import{escHtml as j}from"../../core/utils.js";const le=220,De=56,He=34,Me=3.5;export function renderTrendPanel(g){const{items:G,testDefs:se,onSaveImage:re,onOpen:C,centreLabel:U="mean of all wafers"}=g,ae=g.title??"Wafer-to-wafer trend",{card:l,body:f,controlsRow:Y}=be(ae,re,g.ownerDocument);l.style.alignSelf="start";const H=se.filter(o=>o.testNumber!==void 0&&he(o));let h=g.selectedTestNumber??H[0]?.testNumber??null,I=0;const Q=Le(H,h,o=>{h=o,S(),g.onTestChange?.(o)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:l.ownerDocument});Y.appendChild(Q.el);const O=l.ownerDocument.createElement("span");Object.assign(O.style,{display:"inline-flex",gap:oe.lg,alignItems:"center"}),Y.appendChild(O);const $=we(O,g.axisPrefs,o=>{g.onAxisPrefsChange?.(o),S()},l.ownerDocument),T=l.ownerDocument.createElement("div");Object.assign(T.style,{color:xe.label,fontSize:ge.body,marginBottom:oe.sm}),l.insertBefore(T,f);const L=ve(l);let M=null,n=[];function ce(o){return H.find(B=>B.testNumber===o)}function S(){const{includeLimits:o,clipOutliers:B}=$.get();if(f.innerHTML="",M?.disconnect(),M=null,H.length===0||h===null){T.textContent="",z(f,"No parametric test data available for a trend.");return}if(G.length<2){T.textContent="",z(f,"A wafer-to-wafer trend needs at least two wafers.");return}if(n=fe(G,h),!n.some(r=>r.count>0)){T.textContent="",z(f,"No values recorded for this test.");return}const s=ce(h),x=ue(n),q=()=>{T.textContent=`Point = wafer mean, whisker = \xB11\u03C3, dashed = ${U} \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 J=()=>{const r=Math.max(1,f.clientWidth),d=Math.max(le,Ce(l,f,u,le)),i=Ne(u,l,r,d);if(!i)return;const{ctx:e,theme:b}=i;let P=1/0,N=-1/0;const V=[];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),V.push(t.mean-t.stddev,t.mean+t.stddev));if(!Number.isFinite(P)||!Number.isFinite(N))return;const Z=o??Ae(P,N,s?.limitLow,s?.limitHigh);$.sync(Z,s?.limitLow!==void 0||s?.limitHigh!==void 0);const E=Se({dataMin:P,dataMax:N,limitLow:s?.limitLow,limitHigh:s?.limitHigh,includeLimits:Z,clipOutliers:B,values:V});I=E.clippedCount,q();let p=E.lo,w=E.hi;const ee=(w-p)*.08;p-=ee,w+=ee;const a=X+De;e.font=`${pe(-1)}px system-ui, sans-serif`;const te=t=>t.length>8?`${t.slice(0,7)}\u2026`:t,me=n.length?Math.ceil(Math.max(e.measureText(te(n[0].label)).width,e.measureText(te(n[n.length-1].label)).width)/2):0,y=r-X-me,F=X,k=d-He,v=t=>k-(t-p)/(w-p)*(k-F),ne=n.length>1?(y-a)/(n.length-1):0;A={left:a,step:ne};const R=t=>n.length>1?a+t*ne:(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))}Pe(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),ie=v(t.mean);W?e.lineTo(m,ie):e.moveTo(m,ie),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 de=Math.max(1,Math.ceil(n.length*42/Math.max(1,y-a)));n.forEach((t,c)=>{if(c%de!==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)}),ke(l,f,d)},K=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=K(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>${j(i.label)}</strong><br>`+j(`mean ${D(i.mean,s?.unit)} \xB7 \u03C3 ${D(i.stddev,s?.unit)}`)+`<br>n = ${i.count.toLocaleString()}`+(x!==null?"<br>"+j(`\u0394 vs ${U} ${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",Te(L,l,r.clientX,r.clientY)}),u.addEventListener("mouseleave",()=>{L.style.display="none"}),u.addEventListener("click",r=>{const d=K(r);if(d<0||!C)return;const i=n[d];i.key!==void 0&&h!==null&&C(i.key,h)}),J(),M=ye(f,J)}return S(),{card:l,destroy:()=>{M?.disconnect(),L.remove(),l.remove()},setTest:o=>{Q.set(o)&&(h=o,S())},setAxisPrefs:o=>{$.set(o)&&S()}}}
1
+ import{buildTestTrendData as he,trendCentre as pe}from"../../stats/trend.js";import{isParametricTest as xe}from"../../renderer/buildWaferMap.js";import{fmt as D}from"../../renderer/fmt.js";import{SPACE as le,fontPx as ge,FONT as ye,CLR as be}from"../toolbar.js";import{QUANTITY as X}from"./palette.js";import{cardShell as Te,observeResize as ve,makeTooltip as Le,positionChartTooltip as ke,makeLinkedTestSelect as we,makeLinkedAxisPrefs as Ce,renderEmptyState as z,chartFillHeight as Ae,PADDING as G,resolveAxisRange as Me,shouldIncludeLimitsByDefault as Pe,drawOffAxisLimits as Se,makeAxisFormat as Ne,VERTICAL_TICK_SPACING_PX as He,WAFER_MENU_HINT as Ee,prepareCanvas as Ie}from"./chartShell.js";import{fitTicks as De}from"../../renderer/axisTicks.js";import{escHtml as U}from"../../core/utils.js";const se=220,Oe=56,We=34,$e=3.5;export function renderTrendPanel(f){const{items:j,testDefs:re,onSaveImage:ae,onOpen:C,centreLabel:Y="mean of all wafers"}=f,ce=f.title??"Wafer-to-wafer trend",{card:s,body:u,controlsRow:K}=Te(ce,ae,f.ownerDocument);s.style.alignSelf="start";const N=re.filter(o=>o.testNumber!==void 0&&xe(o));let h=f.selectedTestNumber??N[0]?.testNumber??null,O=0;const Q=we(N,h,o=>{h=o,A(),f.onTestChange?.(o)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:s.ownerDocument});K.appendChild(Q.el);const W=s.ownerDocument.createElement("span");Object.assign(W.style,{display:"inline-flex",gap:le.lg,alignItems:"center"}),K.appendChild(W);const $=Ce(W,f.axisPrefs,o=>{f.onAxisPrefsChange?.(o),A()},s.ownerDocument),w=s.ownerDocument.createElement("div");Object.assign(w.style,{color:be.label,fontSize:ye.body,marginBottom:le.sm}),s.insertBefore(w,u);const g=Le(s);let H=null,i=[];function me(o){return N.find(R=>R.testNumber===o)}function A(){const{includeLimits:o,clipOutliers:R}=$.get();if(u.innerHTML="",H?.disconnect(),H=null,N.length===0||h===null){w.textContent="",z(u,"No parametric test data available for a trend.");return}if(j.length<2){w.textContent="",z(u,"A wafer-to-wafer trend needs at least two wafers.");return}if(i=he(j,h),!i.some(l=>l.count>0)){w.textContent="",z(u,"No values recorded for this test.");return}const r=me(h),y=pe(i),V=()=>{w.textContent=`Point = wafer mean, whisker = \xB11\u03C3, dashed = ${Y} \xB7 wafers in slot order`+(C?" \xB7 click a point to open that wafer":"")+(O?` \xB7 axis clipped, ${O} outside`:"")};V();const d=s.ownerDocument.createElement("canvas");d.style.display="block",u.appendChild(d);let M=null;const q=()=>{const l=Math.max(1,u.clientWidth),a=Math.max(se,Ae(s,u,d,se)),n=Ie(d,s,l,a);if(!n)return;const{ctx:e,theme:b}=n;let P=1/0,S=-1/0;const J=[];for(const t of i)!t.count||!Number.isFinite(t.mean)||(P=Math.min(P,t.mean-t.stddev),S=Math.max(S,t.mean+t.stddev),J.push(t.mean-t.stddev,t.mean+t.stddev));if(!Number.isFinite(P)||!Number.isFinite(S))return;const Z=o??Pe(P,S,r?.limitLow,r?.limitHigh);$.sync(Z,r?.limitLow!==void 0||r?.limitHigh!==void 0);const E=Me({dataMin:P,dataMax:S,limitLow:r?.limitLow,limitHigh:r?.limitHigh,includeLimits:Z,clipOutliers:R,values:J});O=E.clippedCount,V();let x=E.lo,T=E.hi;const ee=(T-x)*.08;x-=ee,T+=ee;const c=G+Oe;e.font=`${ge(-1)}px system-ui, sans-serif`;const te=t=>t.length>8?`${t.slice(0,7)}\u2026`:t,de=i.length?Math.ceil(Math.max(e.measureText(te(i[0].label)).width,e.measureText(te(i[i.length-1].label)).width)/2):0,v=l-G-de,I=G,L=a-We,k=t=>L-(t-x)/(T-x)*(L-I),ne=i.length>1?(v-c)/(i.length-1):0;M={left:c,step:ne};const _=t=>i.length>1?c+t*ne:(c+v)/2;e.strokeStyle=b.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,I),e.lineTo(c,L),e.lineTo(v,L),e.stroke(),e.fillStyle=b.text,e.textAlign="right",e.textBaseline="middle";const ie=De(x,T,L-I,()=>He),fe=Ne(Math.max(Math.abs(x),Math.abs(T)),r?.unit,ie.step||void 0);for(const t of ie.ticks)e.fillText(fe.tickWithUnit(t),c-6,k(t));Se(e,E.offAxis,{left:c,right:v,top:I,bottom:L},"vertical",b.limitLine,t=>D(t,r?.unit));for(const[t,p]of[[r?.limitLow,"Lo limit"],[r?.limitHigh,"Hi limit"]]){if(t===void 0||t<x||t>T)continue;const m=k(t);e.save(),e.strokeStyle=b.limitLine,e.globalAlpha=.7,e.setLineDash([4,3]),e.beginPath(),e.moveTo(c,m),e.lineTo(v,m),e.stroke(),e.restore(),e.save(),e.fillStyle=b.limitLine,e.textAlign="left",e.fillText(p,c+3,m-6),e.restore()}if(y!==null&&y>=x&&y<=T){const t=k(y);e.save(),e.strokeStyle=b.text,e.globalAlpha=.5,e.setLineDash([3,3]),e.beginPath(),e.moveTo(c,t),e.lineTo(v,t),e.stroke(),e.restore()}e.strokeStyle=X,e.lineWidth=1.5,e.beginPath();let B=!1;i.forEach((t,p)=>{if(!t.count||!Number.isFinite(t.mean)){B=!1;return}const m=_(p),oe=k(t.mean);B?e.lineTo(m,oe):e.moveTo(m,oe),B=!0}),e.stroke(),i.forEach((t,p)=>{if(!t.count||!Number.isFinite(t.mean))return;const m=_(p);t.stddev>0&&(e.save(),e.strokeStyle=X,e.globalAlpha=.55,e.lineWidth=1,e.beginPath(),e.moveTo(m,k(t.mean-t.stddev)),e.lineTo(m,k(t.mean+t.stddev)),e.stroke(),e.restore()),e.fillStyle=X,e.beginPath(),e.arc(m,k(t.mean),$e,0,Math.PI*2),e.fill()}),e.fillStyle=b.text,e.textBaseline="top",e.textAlign="center";const ue=Math.max(1,Math.ceil(i.length*42/Math.max(1,v-c)));i.forEach((t,p)=>{if(p%ue!==0&&p!==i.length-1)return;const m=t.label.length>8?`${t.label.slice(0,7)}\u2026`:t.label;e.fillText(m,_(p),L+6)})},F=l=>{if(!M||M.step<=0)return-1;const a=d.getBoundingClientRect(),n=Math.round((l.clientX-a.left-M.left)/M.step);return n>=0&&n<i.length&&i[n].count>0?n:-1};d.addEventListener("mousemove",l=>{const a=F(l);if(a<0){g.style.display="none",d.style.cursor="default";return}const n=i[a];d.style.cursor=C&&n.key!==void 0?"pointer":"default",g.innerHTML=`<strong>${U(n.label)}</strong><br>`+U(`mean ${D(n.mean,r?.unit)} \xB7 \u03C3 ${D(n.stddev,r?.unit)}`)+`<br>n = ${n.count.toLocaleString()}`+(y!==null?"<br>"+U(`\u0394 vs ${Y} ${n.mean-y>=0?"+":""}${D(n.mean-y,r?.unit)}`):"")+(C&&n.key!==void 0?"<br><em>click to open this wafer</em>":"")+(f.onWaferContextMenu&&n.key!==void 0?Ee:""),g.style.display="block",ke(g,s,l.clientX,l.clientY)}),d.addEventListener("mouseleave",()=>{g.style.display="none"}),d.addEventListener("click",l=>{const a=F(l);if(a<0||!C)return;const n=i[a];n.key!==void 0&&h!==null&&C(n.key,h)}),d.addEventListener("contextmenu",l=>{const a=F(l),n=a<0?void 0:i[a].key;n===void 0||!f.onWaferContextMenu||(g.style.display="none",f.onWaferContextMenu(n,h??void 0,l))}),q(),H=ve(u,q)}return A(),{card:s,destroy:()=>{H?.disconnect(),g.remove(),s.remove()},setTest:o=>{Q.set(o)&&(h=o,A())},setAxisPrefs:o=>{$.set(o)&&A()}}}
@@ -1,4 +1,4 @@
1
- import{hasPosition as R,isPositionedDie as O}from"../core/dies.js";import{classifyDie as z}from"../core/classify.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 I,FONT as C,CLR as c,saveTextFile as Y}from"./toolbar.js";import{csvField as T}from"./summaryPanel.js";import{fmt as k}from"../renderer/fmt.js";const _=5e4,F=new WeakSet;function Q(t){if(F.has(t))return;F.add(t);const o=t.createElement("style");o.textContent=`
1
+ import{hasPosition as I,isPositionedDie as B}from"../core/dies.js";import{classifyDie as W}from"../core/classify.js";import{isParametricTest as G,getTestPassStatus as S}from"../renderer/buildWaferMap.js";import{resolveMetadataColumns as Y}from"../stats/metadataColumns.js";import{LEADING as N,TRACKING as _,wireControlHover as U,controlStyle as X,SPACE as Q,RADIUS as F,FONT as g,CLR as c,saveTextFile as q}from"./toolbar.js";import{csvField as H}from"./summaryPanel.js";import{fmt as P}from"../renderer/fmt.js";import{testLabel as E,markedTestLabel as j,derivedFields as J,derivedKeyText as Z,derivedTestSource as ee,isDerivedTest as te,DERIVED_KEY as ne}from"../renderer/testLabel.js";const L=(t,r)=>r?`${t} (${r})`:t,ie=5e4,M=new WeakSet;function le(t){if(M.has(t))return;M.add(t);const r=t.createElement("style");r.textContent=`
2
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;
@@ -9,5 +9,5 @@ import{hasPosition as R,isPositionedDie as O}from"../core/dies.js";import{classi
9
9
  padding: 4px 8px; color: ${c.text};
10
10
  border-bottom: 1px solid ${c.menuBorder}; white-space: nowrap;
11
11
  }
12
- `,t.head.appendChild(o)}function d(t,o,n,a){const i=t.createElement(o);return n&&Object.assign(i.style,n),a!==void 0&&(i.textContent=a),i}function U(t){return R(t)?String(t.x):""}function q(t){return R(t)?String(t.y):""}function J(t,o){if(o!==void 0)return o.filter(a=>a.testNumber!==void 0);const n=new Map;for(const a of t)for(const i of Object.keys(a.testValues??{})){const s=Number(i);n.has(s)||n.set(s,{testNumber:s,name:`Test ${s}`})}return[...n.values()].sort((a,i)=>a.testNumber-i.testNumber)}function Z(t,o,n){if(!o)return;const a=new Map;for(const i of t){if(!O(i))continue;const s=o(i);s&&a.set(i,z(i,s,{ringCount:n}))}return a.size>0?a:void 0}export function buildDieListSection(t,o,n={}){if(!t.length)return null;const a=n.ownerDocument??document;Q(a);const i=J(t,o),s=n.maxRows??_,S=Math.max(0,t.length-s),H=S>0?t.slice(0,s):t,f=d(a,"div",{display:"flex",flexDirection:"column",gap:X.md,flex:"1",minHeight:"0",minWidth:"0"}),w=d(a,"div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});w.appendChild(d(a,"div",{fontSize:C.meta,fontWeight:"700",letterSpacing:W,textTransform:"uppercase",color:c.label},n.title??`Die list (${t.length})`));const g=d(a,"button",{fontSize:C.body,padding:"3px 8px",borderRadius:I.control,...K("outlined"),background:c.menuBg},"Export CSV");g.type="button",G(g),w.appendChild(g),f.appendChild(w),n.note&&f.appendChild(d(a,"div",{fontSize:C.body,color:c.label,lineHeight:D.base,flexShrink:"0"},n.note));const v=Z(t,n.getWafer,n.ringCount??4),P=t.some(e=>e.edgeExcluded===!0),j=[...n.extraColumn?[n.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:t,waferMetadata:n.waferMetadata,metadataFields:n.metadataFields,dieKeys:n.metadataColumns,waferKeys:n.waferMetadataColumns==="none"?"none":void 0,waferPlacement:n.waferMetadataColumns??"csv",reservedLabels:j}),y=[...n.extraColumn?[{label:n.extraColumn.label,get:e=>n.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 k(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 k(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),$=d(a,"div",{overflow:"auto",flex:"1",minHeight:"0",minWidth:"0",border:`1px solid ${c.menuBorder}`,borderRadius:I.control,...n.maxHeight?{maxHeight:n.maxHeight}:{}}),m=a.createElement("table");m.className="wmap-dielist-table",m.setAttribute("aria-label",n.title??`Die list (${t.length} dies)`);const E=d(a,"thead"),A=d(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),m.appendChild(E);const L=d(a,"tbody");for(const e of H){const l=d(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(m.appendChild(L),$.appendChild(m),f.appendChild($),S>0||h.length>0){const e=[];S>0&&e.push(`Showing the first ${s.toLocaleString()} of ${t.length.toLocaleString()} dies. The CSV export contains all ${t.length.toLocaleString()}.`),h.length>0&&e.push(`${h.length} further metadata field${h.length===1?"":"s"} not shown.`),f.appendChild(d(a,"div",{fontSize:C.body,color:c.label,lineHeight:D.base,flexShrink:"0"},e.join(" ")))}return g.addEventListener("click",()=>{const e=[y.map(l=>T(l.label)).join(",")];for(const l of t)e.push(y.map(r=>T((r.csvGet??r.get)(l))).join(","));Y(e.join(`
12
+ `,t.head.appendChild(r)}function s(t,r,n,i){const a=t.createElement(r);return n&&Object.assign(a.style,n),i!==void 0&&(a.textContent=i),a}function ae(t){return I(t)?String(t.x):""}function re(t){return I(t)?String(t.y):""}function oe(t,r){if(r!==void 0)return r.filter(i=>i.testNumber!==void 0);const n=new Map;for(const i of t)for(const a of Object.keys(i.testValues??{})){const d=Number(a);n.has(d)||n.set(d,{testNumber:d,name:E(void 0,d)})}return[...n.values()].sort((i,a)=>i.testNumber-a.testNumber)}function se(t,r,n){if(!r)return;const i=new Map;for(const a of t){if(!B(a))continue;const d=r(a);d&&i.set(a,W(a,d,{ringCount:n}))}return i.size>0?i:void 0}export function buildDieListSection(t,r,n={}){if(!t.length)return null;const i=n.ownerDocument??document;le(i);const a=oe(t,r),d=n.maxRows??ie,C=Math.max(0,t.length-d),z=C>0?t.slice(0,d):t,f=s(i,"div",{display:"flex",flexDirection:"column",gap:Q.md,flex:"1",minHeight:"0",minWidth:"0"}),v=s(i,"div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});v.appendChild(s(i,"div",{fontSize:g.meta,fontWeight:"700",letterSpacing:_,textTransform:"uppercase",color:c.label},n.title??`Die list (${t.length})`));const h=s(i,"button",{fontSize:g.body,padding:"3px 8px",borderRadius:F.control,...X("outlined"),background:c.menuBg},"Export CSV");h.type="button",U(h),v.appendChild(h),f.appendChild(v),n.note&&f.appendChild(s(i,"div",{fontSize:g.body,color:c.label,lineHeight:N.base,flexShrink:"0"},n.note));const w=se(t,n.getWafer,n.ringCount??4),K=t.some(e=>e.edgeExcluded===!0),O=[...n.extraColumn?[n.extraColumn.label]:[],"X","Y","Ring","Quadrant","Edge excluded","Site","Hard bin","Soft bin",...a.map(e=>L(E(e,e.testNumber),e.unit))],{columns:V,truncatedKeys:x}=Y({dies:t,waferMetadata:n.waferMetadata,metadataFields:n.metadataFields,dieKeys:n.metadataColumns,waferKeys:n.waferMetadataColumns==="none"?"none":void 0,waferPlacement:n.waferMetadataColumns??"csv",reservedLabels:O}),y=[...n.extraColumn?[{label:n.extraColumn.label,get:e=>n.extraColumn.get(e)??""}]:[],{label:"X",get:ae},{label:"Y",get:re},...w?[{label:"Ring",get:e=>{const l=w.get(e);return l?String(l.ring):""}},{label:"Quadrant",get:e=>w.get(e)?.quadrant??""}]:[],...K?[{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):""},...a.map(e=>{const l=!G(e);return{label:L(j(e,e.testNumber),e.unit),...te(e)?{csvLabel:`${L(E(e,e.testNumber),e.unit)} [derived from ${ee(e)??ne}]`}:{},get:o=>{if(l){const p=S(o,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=o.testValues?.[e.testNumber];if(u!==void 0)return P(u,e.unit);const b=S(o,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"},csvGet:o=>{if(l){const p=S(o,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=o.testValues?.[e.testNumber];if(u!==void 0)return P(u,void 0,"engineering");const b=S(o,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"}}}),...V.map(e=>({label:e.label,get:e.get,csvOnly:e.csvOnly}))],$=y.filter(e=>!e.csvOnly),A=Z(a.map(e=>({label:j(e,e.testNumber),...J(e)})));A&&f.appendChild(s(i,"div",{fontSize:g.body,color:c.label,lineHeight:N.base,flexShrink:"0"},A));const D=s(i,"div",{overflow:"auto",flex:"1",minHeight:"0",minWidth:"0",border:`1px solid ${c.menuBorder}`,borderRadius:F.control,...n.maxHeight?{maxHeight:n.maxHeight}:{}}),m=i.createElement("table");m.className="wmap-dielist-table",m.setAttribute("aria-label",n.title??`Die list (${t.length} dies)`);const R=s(i,"thead"),T=s(i,"tr");for(const e of $){const l=i.createElement("th");l.className="wmap-dielist-th",l.scope="col",l.textContent=e.label,T.appendChild(l)}R.appendChild(T),m.appendChild(R);const k=s(i,"tbody");for(const e of z){const l=s(i,"tr");for(const o of $){const u=i.createElement("td");u.className="wmap-dielist-td",u.textContent=o.get(e),l.appendChild(u)}k.appendChild(l)}if(m.appendChild(k),D.appendChild(m),f.appendChild(D),C>0||x.length>0){const e=[];C>0&&e.push(`Showing the first ${d.toLocaleString()} of ${t.length.toLocaleString()} dies. The CSV export contains all ${t.length.toLocaleString()}.`),x.length>0&&e.push(`${x.length} further metadata field${x.length===1?"":"s"} not shown.`),f.appendChild(s(i,"div",{fontSize:g.body,color:c.label,lineHeight:N.base,flexShrink:"0"},e.join(" ")))}return h.addEventListener("click",()=>{const e=[y.map(l=>H(l.csvLabel??l.label)).join(",")];for(const l of t)e.push(y.map(o=>H((o.csvGet??o.get)(l))).join(","));q(e.join(`
13
13
  `),"die-list.csv","text/csv",n.onSaveText)}),f}
@@ -0,0 +1,23 @@
1
+ import { type SweepSpec } from '../stats/sweep.js';
2
+ import { type SaveImageHandler } from './toolbar.js';
3
+ import type { DrilldownSource, DrilldownItem } from './chartPopulation.js';
4
+ export type { DrilldownSource, DrilldownItem };
5
+ /** What the targets can draw on — the host's chart definitions and hooks. */
6
+ export interface DrilldownContext {
7
+ sweeps?: SweepSpec[];
8
+ onSaveImage?: SaveImageHandler;
9
+ }
10
+ /**
11
+ * Open the drilldown menu for `source` at viewport point `at`.
12
+ *
13
+ * `anchor` is an on-page element of the triggering render (the map canvas, or
14
+ * the toolbar button) — it resolves the menu layer and the chart modal's root,
15
+ * and gets focus back when the menu is dismissed with Escape. Returns a close
16
+ * function; the menu also closes on an outside press, Escape, Tab, or when a
17
+ * chart is picked.
18
+ */
19
+ export declare function openDrilldownMenu(at: {
20
+ x: number;
21
+ y: number;
22
+ }, anchor: HTMLElement, source: DrilldownSource, ctx: DrilldownContext): () => void;
23
+ //# sourceMappingURL=drilldown.d.ts.map
@@ -0,0 +1 @@
1
+ import{isYieldEligibleDie as b}from"../core/dies.js";import{isParametricTest as y}from"../renderer/buildWaferMap.js";import{buildSweepData as T}from"../stats/sweep.js";import{renderSweepPanel as C}from"./charts/sweep.js";import{renderHistogramPanel as E}from"./charts/histogram.js";import{renderCapabilityPanel as $}from"./charts/capability.js";import{openChartExpandModal as M,populationPhrase as w}from"./charts/chartShell.js";import{buildCheckMenuEl as I,createToolbarHelpers as x,getTooltip as N,menuLayerFor as R,wireMenuA11y as L,CLR as V,FONT as k,SPACE as F}from"./toolbar.js";const P=30,v=e=>e.items.flatMap(t=>t.dies);function O(e){return e.items.map(t=>({...t,dies:t.dies.filter(a=>b(a))}))}function A(e,t,a){return(e.testDefs??[]).filter(y).map(s=>s.testNumber).filter(s=>{let o=0;for(const i of t){const n=i.testValues?.[s];if(n!==void 0&&Number.isFinite(n)&&++o>=a)return!0}return!1})}function S(e,t,a,s,o){const n=e.ownerDocument.createElement("div");n.textContent=a,n.dataset.wmapPopulation="1",Object.assign(n.style,{color:V.label,fontSize:k.body,marginBottom:F.sm});const r=e.querySelector("[data-wmap-chart-expand]")?.parentElement;r?.parentElement===e?r.after(n):e.prepend(n),M(e,t,{anchor:s,onClosed:o})}function H(e,t,a,s){const o=v(t);let i=t.notMeasuredReason??null;return!i&&!o.some(n=>b(n))&&(i="These dies are all partial or edge-excluded, which charts leave out"),i||T(o,t.testDefs,e).series.some(r=>r.points.some(d=>d.count>0))||(i="These dies have no values for this sweep\u2019s tests"),{section:"Sweep",label:e.title,unavailable:i,open:()=>{const n=C({spec:e,dies:o,population:t.population,testDefs:t.testDefs,onSaveImage:a.onSaveImage,ownerDocument:s.ownerDocument}),r=o.filter(d=>b(d)).length;M(n.card,`${e.title} \u2014 ${w(r,o.length,t.population)}`,{anchor:s,onClosed:()=>n.destroy()})}}}function W(e,t,a){const s=O(e),o=v(e).length,i=s.flatMap(l=>l.dies),n=w(i.length,o,e.population),r=(e.testDefs??[]).filter(y),d=a.ownerDocument,f=(l,m)=>e.notMeasuredReason?e.notMeasuredReason:i.length===0?"These dies are all partial or edge-excluded, which charts leave out":A(e,i,l).length>0?null:m,u={section:"Distributions",label:"Value histogram",unavailable:f(1,"These dies have no parametric test values"),open:()=>{const l=E({title:"Value histogram",items:s,testDefs:r,selectedTestNumber:e.activeTest,onSaveImage:t.onSaveImage,ownerDocument:d});S(l.card,`Value histogram \u2014 ${n}`,`Population: ${n}`,a,()=>l.destroy())}},h={section:"Distributions",label:"Process capability",unavailable:f(2,"Needs at least 2 of these dies with values for one test"),open:()=>{const l=$({title:"Process capability",items:s,testDefs:r,selectedTestNumber:e.activeTest,onSaveImage:t.onSaveImage,ownerDocument:d}),m=i.length<P?` \xB7 fewer than ${P} dies, so each Ppk is a rough estimate`:"";S(l.card,`Process capability \u2014 ${n}`,`Population: ${n}${m}`,a,()=>l.destroy())}};return[u,h]}function B(e,t,a){return[...W(e,t,a),...(t.sweeps??[]).map(s=>H(s,e,t,a))]}const g=new WeakMap;export function openDrilldownMenu(e,t,a,s){const o=t.ownerDocument,i=o.defaultView??window;g.get(o)?.();const n=B(a,s,t),r=[];let d;for(const p of n)p.section!==d&&(r.push({section:p.section}),d=p.section),r.push({label:p.label,active:!1,action:!0,enabled:p.unavailable===null,disabledHint:p.unavailable??void 0,onClick:()=>{c(),p.open()}});const{makeMenuSection:f}=x(N(o)),u=I(new DOMRect(e.x,e.y-4,0,0),r,{makeMenuSection:f},i),h=v(a).length;u.setAttribute("role","menu"),u.setAttribute("aria-label",`Open a chart of ${w(h,h,a.population)}`),u.dataset.wmapDrilldownMenu="1",R(t).appendChild(u);const l=u.getBoundingClientRect();l.bottom>i.innerHeight-4&&(u.style.top=`${Math.max(4,e.y-l.height)}px`),l.right>i.innerWidth-4&&(u.style.left=`${Math.max(4,i.innerWidth-l.width-4)}px`);let m=!1;function c(){m||(m=!0,u.remove(),o.removeEventListener("pointerdown",D,!0),i.removeEventListener("blur",c),g.get(o)===c&&g.delete(o))}function D(p){u.contains(p.target)||c()}return o.addEventListener("pointerdown",D,!0),i.addEventListener("blur",c),L(u,t,c),g.set(o,c),c}
@@ -2,7 +2,7 @@ export const ICONS={aggr:'<svg viewBox="0 0 24 24" width="16" height="16" fill="
2
2
  <rect x="3" y="3" width="4" height="18" rx="1"/>
3
3
  <rect x="10" y="3" width="4" height="18" rx="1"/>
4
4
  <rect x="17" y="3" width="4" height="18" rx="1"/>
5
- </svg>`,download:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"/><circle cx="12" cy="13" r="3"/></svg>',downloadAll:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"/><circle cx="12" cy="13" r="3"/><circle cx="5.5" cy="4.5" r="0.8" fill="currentColor" stroke="none"/><circle cx="8" cy="4.5" r="0.8" fill="currentColor" stroke="none"/><circle cx="5.5" cy="7" r="0.8" fill="currentColor" stroke="none"/><circle cx="8" cy="7" r="0.8" fill="currentColor" stroke="none"/></svg>',expand:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>',findings:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M12 2v4"/><path d="M16 2v4"/><rect width="16" height="18" x="4" y="4" rx="2"/><path d="M8 10h6"/><path d="M8 14h8"/><path d="M8 18h5"/></svg>',flipH:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 5 5-5 5V7"/><path d="m21 7-5 5 5 5V7"/><path d="M12 20v2"/><path d="M12 14v2"/><path d="M12 8v2"/><path d="M12 2v2"/></svg>',flipV:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m17 3-5 5-5-5h10"/><path d="m17 21-5-5-5 5h10"/><path d="M4 12H2"/><path d="M10 12H8"/><path d="M16 12h-2"/><path d="M22 12h-2"/></svg>',help:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>',labels:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v16"/><path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2"/><path d="M9 20h6"/></svg>',legend:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/><path d="M14 4h7"/><path d="M14 9h7"/><path d="M14 15h7"/><path d="M14 20h7"/></svg>',logScale:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21 L5 3"/><path d="M3.5 5 L5 3 L6.5 5"/><path d="M5 21 L21 21"/><path d="M19 19.5 L21 21 L19 22.5"/><path d="M6 20 C 6 12, 10 7, 20 6"/></svg>',maximize:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>',minimize:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/></svg>',mode:`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
5
+ </svg>`,download:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"/><circle cx="12" cy="13" r="3"/></svg>',downloadAll:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"/><circle cx="12" cy="13" r="3"/><circle cx="5.5" cy="4.5" r="0.8" fill="currentColor" stroke="none"/><circle cx="8" cy="4.5" r="0.8" fill="currentColor" stroke="none"/><circle cx="5.5" cy="7" r="0.8" fill="currentColor" stroke="none"/><circle cx="8" cy="7" r="0.8" fill="currentColor" stroke="none"/></svg>',drilldown:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 16c3-1 4-9 7-9s3 5 6 5"/></svg>',expand:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>',findings:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M12 2v4"/><path d="M16 2v4"/><rect width="16" height="18" x="4" y="4" rx="2"/><path d="M8 10h6"/><path d="M8 14h8"/><path d="M8 18h5"/></svg>',flipH:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 5 5-5 5V7"/><path d="m21 7-5 5 5 5V7"/><path d="M12 20v2"/><path d="M12 14v2"/><path d="M12 8v2"/><path d="M12 2v2"/></svg>',flipV:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m17 3-5 5-5-5h10"/><path d="m17 21-5-5-5 5h10"/><path d="M4 12H2"/><path d="M10 12H8"/><path d="M16 12h-2"/><path d="M22 12h-2"/></svg>',help:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>',labels:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v16"/><path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2"/><path d="M9 20h6"/></svg>',legend:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/><path d="M14 4h7"/><path d="M14 9h7"/><path d="M14 15h7"/><path d="M14 20h7"/></svg>',logScale:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21 L5 3"/><path d="M3.5 5 L5 3 L6.5 5"/><path d="M5 21 L21 21"/><path d="M19 19.5 L21 21 L19 22.5"/><path d="M6 20 C 6 12, 10 7, 20 6"/></svg>',maximize:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>',minimize:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/></svg>',mode:`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
6
6
  <ellipse cx="12" cy="7" rx="9" ry="4" />
7
7
 
8
8
  <g stroke="currentColor" stroke-width="1.4" opacity="0.85">
@@ -2,7 +2,8 @@ import type { LotStatsSummary } from '../stats/types.js';
2
2
  import type { WaferMapDisplayItem } from './renderWaferGallery.js';
3
3
  import type { BinColors } from '../renderer/binColors.js';
4
4
  import { type SaveImageHandler, type SaveTextHandler } from './toolbar.js';
5
- export type InsightsView = 'overview' | 'distributions' | 'correlation';
5
+ import { type SweepSpec } from '../stats/sweep.js';
6
+ export type InsightsView = 'overview' | 'distributions' | 'correlation' | 'sweeps';
6
7
  /** Public option shape for `RenderOptions.insights`/`GalleryOptions.insights`. */
7
8
  export interface InsightsOptions {
8
9
  /**
@@ -10,11 +11,68 @@ export interface InsightsOptions {
10
11
  * grid with wmap's own chart suite across three sub-tabs — Overview
11
12
  * (yield, bins, ring/quadrant yield, test values), Distributions
12
13
  * (process capability, boxplot, histogram), and Correlation (matrix +
13
- * scatter). Default false.
14
+ * scatter) — plus a fourth, Sweeps, when `sweeps` defines any. Default false.
14
15
  */
15
16
  enabled?: boolean;
16
- /** Which sub-tab is shown first. Default 'overview'. */
17
+ /** Which sub-tab is shown first. Default 'overview'. `'sweeps'` with no
18
+ * `sweeps` defined falls back to 'overview' — there is no such tab to open. */
17
19
  defaultView?: InsightsView;
20
+ /**
21
+ * Parametric sweeps, one card each in their own Sweeps sub-tab — which appears
22
+ * only when this is non-empty.
23
+ *
24
+ * A sweep reads an ordered run of tests as a response curve rather than as
25
+ * independent tests, and measures the PAIR: where the first two series cross,
26
+ * and how far apart they are at given levels. The case it exists for is the
27
+ * same quantity measured at a series of power levels, recorded as a block of
28
+ * consecutive test numbers, swept up in one block and down in another.
29
+ *
30
+ * ```ts
31
+ * insights: { enabled: true, sweeps: [{
32
+ * id: 'power', title: 'Power Sweep',
33
+ * series: [
34
+ * { label: 'Rising', tests: [1010, 1011, 1012], xValues: [0, 5, 10] },
35
+ * { label: 'Falling', tests: [1020, 1021, 1022], xValues: [0, 5, 10] },
36
+ * ],
37
+ * separationAt: [1.2, 2.5],
38
+ * }] }
39
+ * ```
40
+ *
41
+ * Deliberately carries no population scope of its own: a sweep definition
42
+ * says which tests form the curve, and is therefore valid for any population
43
+ * and portable between hosts. The dies it aggregates are whatever the
44
+ * Insights view is currently scoped to.
45
+ *
46
+ * The same definitions drive drilldown: a user can select dies on a map and
47
+ * right-click (or use the toolbar's "Chart the selection") to sweep just
48
+ * those dies. A single map offers this even when `enabled` is off.
49
+ *
50
+ * **Provisional.** This arrived as one site's request. The mechanism — an
51
+ * ordered run of tests read as a curve — is a recurring semiconductor shape
52
+ * (shmoo, VDD/temperature sweeps, retention, endurance, IV), which is the case
53
+ * for it being library-level; against it is that only one host has asked. It
54
+ * is shipping so tsmap can put it in front of that user, and the answer
55
+ * decides it: a second sweep-shaped use means it is general and stays, while
56
+ * "also measure X, and split by Y" means it is a bespoke chart and belongs
57
+ * behind a host-contributed-panel extension point instead. `separationAt` is
58
+ * the narrowest part of the surface and the first thing to drop if it is not
59
+ * used. Do not widen this shape before that question is settled.
60
+ *
61
+ * Range strings in `tests` (`"1010..1030"`) are NOT a widening in that sense:
62
+ * they are input syntax for the same list of tests, and add nothing to what a
63
+ * sweep measures. They were added because ranges are the first thing anyone
64
+ * writing a sweep by hand asks for, and they share the derived-test parser so
65
+ * the syntax is one syntax library-wide.
66
+ */
67
+ sweeps?: SweepSpec[];
68
+ /**
69
+ * Offers a "Remove" button on the Sweeps tab's notice about sweeps that name
70
+ * none of the data's tests — typically sweeps a host kept from another test
71
+ * program. Called with those sweeps' ids; the host drops them and re-renders
72
+ * with the remaining `sweeps`. Without it the notice still shows, with no
73
+ * button. The library never edits the host's sweep list itself.
74
+ */
75
+ onRemoveSweeps?: (ids: string[]) => void;
18
76
  /**
19
77
  * Open the Insights view on mount instead of starting on the map/grid.
20
78
  * Default false — the tab is offered, the map is what you see first.
@@ -71,6 +129,10 @@ export interface InsightsTabDeps {
71
129
  focusTest?: (testNumber: number) => void;
72
130
  /** Default sub-tab shown on first render. Default 'overview'. */
73
131
  defaultView?: InsightsView;
132
+ /** Sweep definitions to render in the Distributions view — see `InsightsOptions.sweeps`. */
133
+ sweeps?: SweepSpec[];
134
+ /** See `InsightsOptions.onRemoveSweeps`. */
135
+ onRemoveSweeps?: (ids: string[]) => void;
74
136
  /**
75
137
  * When provided, the tab bar gets a leading "‹ Map"/"‹ Gallery" tab that
76
138
  * exits Insights back to the host's normal view — one visible navigation
@@ -1 +1 @@
1
- import{buildFacetTable as $e,facetValueOf as Le,FACET_NONE_VALUE as Ee}from"../stats/facets.js";import{mergeTestDefs as Ie}from"../stats/mergeTestDefs.js";import{isParametricTest as Oe}from"../renderer/buildWaferMap.js";import{waferDisplayLabel as Pe}from"../core/waferLabel.js";import{INPUT_DEFAULT_PASS_BINS as R,itemPassBins as Ae,passBinsLabel as ae}from"../core/passBins.js";import{NO_DATA_FILL as oe}from"../renderer/colorMap.js";import{describeWaferPopulation as Re,populationStat as We}from"../stats/population.js";import{LEADING as je,ALPHA as Ge,SPACE as w,EDGE_GUTTER as X,FONT as G,CLR as B,RADIUS as Ye,SHADOW as He,controlStyle as Fe,wireControlHover as re,wireTooltip as Me}from"./toolbar.js";import{ICONS as Ne}from"./icons.js";import{renderCapabilityPanel as Ve}from"./charts/capability.js";import{renderBoxplotPanel as _e}from"./charts/boxplot.js";import{renderTrendPanel as Ue}from"./charts/trend.js";import{renderHistogramPanel as ze}from"./charts/histogram.js";import{renderCorrelationPanel as qe}from"./charts/correlation.js";import{renderScatterPanel as Qe}from"./charts/scatter.js";import{renderBarPanel as se}from"./charts/barPanel.js";import{renderBinClusterPanel as Ke}from"./charts/binCluster.js";import{renderTestPassRatePanel as Xe}from"./charts/testPassRate.js";import{QUANTITY as Je}from"./charts/palette.js";import{cardFrameStyle as Ze,makeChartGridWrap as J,makeLabeledSelect as et,makeLinkedGroupSelect as tt}from"./charts/chartShell.js";import{buildYieldData as V,buildYieldDataCombined as ie}from"../stats/yield.js";import{buildBinParetoData as nt}from"../stats/binPareto.js";import{buildLotTestSection as at,buildLotFunctionalSection as ot,buildMetadataStripBox as rt}from"./summaryPanel.js";import{buildRegionYieldData as le,buildRingRegions as st,buildQuadrantRegions as it}from"../stats/regions.js";import{renderRegionYieldDiagram as de}from"./charts/regionYieldDiagram.js";const lt=[{key:"overview",label:"Overview"},{key:"distributions",label:"Distributions"},{key:"correlation",label:"Correlation"}];export function createInsightsTab(D){const{getItems:ce,getLotStats:pe,getBinColors:Z,getRingCount:ue,onSaveImage:T,onSaveText:_,openWafer:Y,focusTest:ee}=D,fe=D.showMetadataStrip??!0,W=D.contentInset??X,l=D.ownerDocument??document,O=l.createElement("div");Object.assign(O.style,{display:"none",flexDirection:"column",width:"100%",paddingTop:"0"});const me=X,P=l.createElement("div");Object.assign(P.style,{paddingLeft:me});const k=l.createElement("div");Object.assign(k.style,{display:"flex",gap:w.xs,alignItems:"center",background:B.menuBg,border:`1px solid ${B.menuBorder}`,borderRadius:Ye.container,boxShadow:He.panel,padding:`0 ${w.lg}`,marginBottom:w.lg,marginLeft:W,marginRight:W}),k.setAttribute("role","tablist"),k.addEventListener("keydown",e=>{if(e.key!=="ArrowLeft"&&e.key!=="ArrowRight")return;const t=Array.from(k.querySelectorAll('[role="tab"]')),r=t.indexOf(l.activeElement);if(r===-1)return;e.preventDefault();const d=t[(r+(e.key==="ArrowRight"?1:-1)+t.length)%t.length];d.focus(),d.click()});const j=l.createElement("div");Object.assign(j.style,{display:"flex",flexDirection:"column",gap:w.lg,marginTop:"0",paddingLeft:W,paddingRight:W}),O.appendChild(P),O.appendChild(k),O.appendChild(j);let I=D.defaultView??"overview",A,L=null,$=null,H={includeLimits:void 0,clipOutliers:!1};const U=e=>{$!==e&&($=e,M())};let F=new Map;function te(){for(const e of F.values())e.destroy();F=new Map}function z(e,t,r){Y?.(e,t,r)}function be(e){return Y?{open:(t,r)=>z(t,`Wafer ${e.find(d=>d.waferIndex===t)?.label??t}`,r)}:ee&&e.length===1?{open:(t,r)=>ee(r),label:"show this test on the map"}:null}function ge(){return ce().map((e,t)=>e==null?null:{metadata:e.wafer.metadata??void 0,dies:e.dies,wafer:e.wafer,label:Pe(e,t),waferIndex:t,passBins:Ae(e),statsSummary:e.statsSummary,testDefs:e.testDefs}).filter(e=>e!=null)}function ne(e,t){Object.assign(e.style,{background:"none",border:"none",borderBottom:t?`2px solid ${B.iconActive}`:"2px solid transparent",color:t?B.iconActive:B.label,fontWeight:t?"700":"500",fontSize:G.body,padding:`${w.sm} ${X} ${w.md}`,cursor:"pointer",marginBottom:"-1px"})}function ye(e,t){const r=l.createElement("button");r.type="button",r.textContent=t;const d=e===I;return ne(r,d),r.dataset.on=d?"true":"false",re(r,"bare"),r.dataset.wmapInsightsTab=e,r.setAttribute("role","tab"),r.setAttribute("aria-selected",d?"true":"false"),r.tabIndex=d?0:-1,r.addEventListener("click",()=>{I!==e&&(I=e,M({keepSections:!0}))}),r}function he(e){const t=l.createElement("button");return t.type="button",t.textContent=`\u2039 ${e.label}`,ne(t,!1),t.dataset.wmapInsightsBack="1",t.addEventListener("click",e.onBack),t}function Ce(e,t,r,d=!0){const c=J(l),p=Z();let m="label";const b=r??"group",u=ae(e.map(i=>i.passBins)),s=pe?.(),g=new Map(s?.lotYieldSeries.map(i=>[i.waferIndex,i.yieldPercent])),S=i=>({...i,label:i.label.replace(/\s*·\s*\d+(\.\d+)?%$/,""),yieldPercent:g.get(i.waferIndex),key:i.waferIndex}),f=e.map(S),y=t?.map(i=>({key:i.key,items:i.items.map(S)})),x=()=>y?ie(y,R,m):V(f,R,m),C={title:t?`Yield by ${b} (pass: ${u})`:`Yield by wafer (pass: ${u})`,data:x(),selfControl:{current:m,options:[["yield","Sort: yield"],["label",t?`Sort: ${b}`:"Sort: wafer ID"]],onChange:i=>(m=i,{data:x()})},barColor:()=>Je,valueLabel:i=>`${i.percent.toFixed(1)}%`,reference:i=>{if(i.length<3)return null;const v=i.map(K=>K.percent).sort((K,De)=>K-De),E=v.length%2?v[(v.length-1)/2]:(v[v.length/2-1]+v[v.length/2])/2;return{value:E,label:`median ${E.toFixed(1)}%`}},drill:y?{onOpenGroup:i=>{const v=y.find(E=>E.key===i.label)?.items??[];return{data:V(v,R,m),title:`Yield by wafer \u2014 ${b}: ${i.label} (pass: ${u})`}},onBack:()=>({data:x(),title:`Yield by ${b} (pass: ${u})`}),groupLabelText:b}:void 0,onOpen:Y?i=>{typeof i.key=="number"&&z(i.key,`Wafer ${i.label}`)}:void 0,ownerDocument:l};let n=null;if(d&&(n=se(C,T),n.card.style.minHeight="360px",c.appendChild(n.card)),t){const i=Ke({title:"Hard bin pareto",groups:t.map(v=>({key:v.key,items:v.items.map(E=>({dies:E.dies,hardBinCounts:E.statsSummary?.stats.hardBinCounts,softBinCounts:E.statsSummary?.stats.softBinCounts}))})),onSaveImage:T,ownerDocument:l});return i.card.style.minHeight="360px",c.appendChild(i.card),{card:c,destroy:()=>{n?.destroy(),i.destroy()}}}let a="hbin";const o=i=>i===void 0?oe:(a==="hbin"?p.hard:p.soft).get(i)??oe,h=e.map(i=>({dies:i.dies,hardBinCounts:i.statsSummary?.stats.hardBinCounts,softBinCounts:i.statsSummary?.stats.softBinCounts})),N=()=>nt(h,a),Be={title:"Hard bin pareto",data:N(),selfControl:{current:a,options:[["hbin","Hard bins"],["sbin","Soft bins"]],onChange:i=>(a=i,{data:N(),title:`${a==="hbin"?"Hard":"Soft"} bin pareto`})},barColor:i=>o(i.binCode),ownerDocument:l},Q=se(Be,T);return Q.card.style.minHeight="360px",c.appendChild(Q.card),{card:c,destroy:()=>{n?.destroy(),Q.destroy()}}}function q(){const e=l.createElement("div");return e.dataset.wmapChartCard="1",Object.assign(e.style,{...Ze(),display:"flex",flexDirection:"column",gap:w.lg,minWidth:"0"}),e}function Se(e){if(P.innerHTML="",P.style.marginBottom="0",!fe)return;const t=rt(e.map(r=>({metadata:r.metadata})),void 0,{facetableOnly:!1});t&&(t.style.marginBottom="0",P.appendChild(t),P.style.marginBottom=w.xs)}function xe(e,t,r,d){if(!e.length)return{elements:[],testValuesCard:null,functionalCard:null,destroy:()=>{}};const c=e.map(o=>o.wafer),p=e.map(o=>o.dies),m=e.flatMap(o=>o.dies),b=e.map(o=>o.statsSummary),u=b.every(o=>o!==void 0)?b:void 0,s=ue?.()??4,g=[],S=[],y=Xe({groups:(d??[{key:"",items:e}]).map(o=>({key:o.key,items:o.items.map(h=>({dies:h.dies,testSpecYield:h.statsSummary?.stats.testSpecYield,functionalYield:h.statsSummary?.stats.functionalYield}))})),testDefs:r,onSaveImage:T,ownerDocument:l});y.card.style.minHeight="300px",g.push(y.card),S.push(y.destroy);const x=le(p,c,s,o=>e[o].passBins,st);if(x.length){const o=de({title:"Ring yield",mode:"ring",rows:x,onSaveImage:T,ownerDocument:l});g.push(o.card),S.push(o.destroy)}const C=le(p,c,s,o=>e[o].passBins,it);if(C.length){const o=de({title:"Quadrant yield",mode:"quadrant",rows:C,onSaveImage:T,ownerDocument:l});g.push(o.card),S.push(o.destroy)}let n=null;if(t.length){const o=at(m,t,void 0,u,_,p.map(h=>({dies:h})),void 0,"full");if(o){const h=q();h.appendChild(o),n=h}}let a=null;if(r.length){const o=ot(m,r,u,_);if(o){const h=q();h.appendChild(o),a=h}}return{elements:g,testValuesCard:n,functionalCard:a,destroy:()=>{for(const o of S)o()}}}function we(e){const t=ae(e.map(s=>s.passBins)),r=e.length===1,d=e[0],c=q();Object.assign(c.style,{flexDirection:"row",flexWrap:"wrap",gap:w.md});const p=s=>{const g=s.lastElementChild;return g&&Object.assign(g.style,{borderRight:"none",paddingRight:"0",marginRight:"0"}),s};function m(s,g,S){const f=l.createElement("div");Object.assign(f.style,{display:"flex",alignItems:"baseline",gap:w.sm,paddingRight:w.xxl,marginRight:w.md,borderRight:`1px solid ${B.menuBorder}`});const y=l.createElement("div");y.textContent=s,Object.assign(y.style,{fontSize:G.heading,fontWeight:"700",color:B.value,lineHeight:je.tight});const x=l.createElement("div");if(x.textContent=g,Object.assign(x.style,{fontSize:G.body,color:B.label}),f.append(y,x),S){const C=l.createElement("div");C.textContent=S,Object.assign(C.style,{fontSize:G.meta,color:B.label,opacity:Ge.muted}),f.appendChild(C)}return f}if(r){const s=V([{...d,key:d.waferIndex}],R)[0]?.percent;return s!==void 0&&c.appendChild(m(`${s.toFixed(1)}%`,`Yield \xB7 pass: ${t}`)),c.appendChild(m(String(d.dies.length),"Total dies")),p(c)}c.appendChild(m(String(e.length),"Wafers"));const b=V(e.map(s=>({...s,key:s.waferIndex})),R).map(s=>s.percent).filter(s=>Number.isFinite(s));if(b.length){const s=b.reduce((f,y)=>f+y,0)/b.length,g=ie([{key:"all",items:e.map(f=>({...f,key:f.waferIndex}))}],R)[0]?.percent,S=g!==void 0&&g.toFixed(1)!==s.toFixed(1);c.appendChild(m(`${s.toFixed(1)}%`,`Mean per-wafer yield \xB7 pass: ${t}`,S?`${g.toFixed(1)}% by dies`:void 0))}const u=e.map(s=>s.statsSummary);if(u.every(s=>s!==void 0)){const s=u.reduce((S,f)=>S+f.stats.analyzedDies,0),g=u.reduce((S,f)=>S+f.stats.excludedDies,0);c.appendChild(m(s.toLocaleString(),"Dies analysed",g?`${g.toLocaleString()} excluded`:void 0))}else c.appendChild(m(e.reduce((s,g)=>s+g.dies.length,0).toLocaleString(),"Total dies"));return p(c)}function Te(e,t,r,d,c){const p=l.createElement("div");Object.assign(p.style,{display:"flex",flexDirection:"column",gap:w.lg});const m=e.length===1&&!d;e.length&&!d&&p.appendChild(we(e));const b=Ce(e,d,c,!m);p.appendChild(b.card);const u=xe(e,t,r,d);for(const s of u.elements)b.card.appendChild(s);return u.testValuesCard&&p.appendChild(u.testValuesCard),u.functionalCard&&p.appendChild(u.functionalCard),{card:p,destroy:()=>{b.destroy(),u.destroy()}}}function ve(e,t,r,d){const c=J(l),p=be(e),m=a=>{H=a;for(const o of[f,y,x])o!==b&&o?.setAxisPrefs(a)};let b=null;const u=a=>o=>{b=a(),m(o),b=null},s=a=>({...a,testStats:a.statsSummary?.stats.perTestStats}),g=e.map(s),S=r?.map(a=>({key:a.key,items:a.items.map(s)})),f=_e({title:"Test value distribution",axisPrefs:H,onAxisPrefsChange:u(()=>f),onTestChange:a=>C(a),selectedTestNumber:L??void 0,items:g,testDefs:t,groups:S,groupLabelText:d,onSaveImage:T,onGroupChange:a=>U(a),onOpen:p?.open,openActionLabel:p?.label,ownerDocument:l}),y=ze({title:"Value histogram",axisPrefs:H,onAxisPrefsChange:u(()=>y),onTestChange:a=>C(a),selectedTestNumber:L??void 0,items:e,testDefs:t,groups:r,onGroupChange:a=>U(a),onSaveImage:T,ownerDocument:l}),x=Ue({title:"Wafer-to-wafer trend",axisPrefs:H,onAxisPrefsChange:u(()=>x),onTestChange:a=>C(a),selectedTestNumber:L??void 0,items:e.map(a=>({label:a.label,key:a.waferIndex,dies:a.dies,testStats:a.statsSummary?.stats.perTestStats})),testDefs:t,onSaveImage:T,centreLabel:We(Re(e.map(a=>a.wafer.metadata)),"mean"),onOpen:Y?(a,o)=>z(a,`Wafer ${e.find(h=>h.waferIndex===a)?.label??a}`,o):void 0,ownerDocument:l}),C=a=>{L=a,f.setTest(a),y.setTest(a),x.setTest(a),n.setTest(a)},n=Ve({title:"Process capability",items:e,testDefs:t,onSaveImage:T,selectedTestNumber:L??void 0,onSelectTest:a=>C(a),ownerDocument:l});return c.append(n.card,f.card,y.card,x.card),{card:c,destroy:()=>{n.destroy(),f.destroy(),y.destroy(),x.destroy()}}}function ke(e,t,r){const d=J(l),c=Qe({title:"Test scatter",items:e,testDefs:t,groups:r,binColors:Z().hard,onSaveImage:T,ownerDocument:l}),p=qe({title:"Test correlation matrix",items:e,testDefs:t,onSaveImage:T,onSaveText:_,onSelectPair:(m,b)=>c.setXY(m,b),ownerDocument:l});return d.append(p.card,c.card),{card:d,destroy:()=>{p.destroy(),c.destroy()}}}function M(e){e?.keepSections||te(),j.innerHTML="",k.innerHTML="",D.backTab&&k.appendChild(he(D.backTab));for(const n of lt)k.appendChild(ye(n.key,n.label));const t=ge();Se(t);const r=$e(t,{facetableOnly:!0}).filter(n=>n.splittable),d=l.createElement("div");Object.assign(d.style,{display:"flex",gap:w.md,alignItems:"center",marginLeft:w.xxl,paddingBottom:w.xs});let c;r.length>0?(d.appendChild(et("Group by:",[{value:"",label:"None"},...r.map(n=>({value:n.key,label:`${n.label} (${n.values.length})`}))],A??"",n=>{A=n||void 0,$=null,M()},{hook:"group-by",ownerDocument:l})),c=r.find(n=>n.key===A)?.label):A=void 0;let p;if(A){const n=new Map,a=[];for(const o of t){const h=Le(o.metadata,A)??Ee;n.has(h)||(n.set(h,[]),a.push(h)),n.get(h).push(o)}p=a.map(o=>({key:o,items:n.get(o)}))}$!==null&&!p?.some(n=>n.key===$)&&($=null),p&&p.length>0&&d.appendChild(tt(p.map(n=>n.key),"Show:",$,n=>U(n),{ownerDocument:l}).el);let m=null;if(D.onOpenGuide){const n=l.createElement("button");n.type="button",n.innerHTML=Ne.help,n.setAttribute("aria-label","User guide"),Object.assign(n.style,{...Fe("bare"),display:"inline-flex",alignItems:"center",justifyContent:"center",width:"24px",height:"24px",flexShrink:"0"}),re(n,"bare"),Me(n,"User guide"),n.addEventListener("click",()=>D.onOpenGuide()),Object.assign(n.style,{marginLeft:"auto"}),m=n}k.appendChild(d),m&&k.appendChild(m);const b=$!==null?p?.find(n=>n.key===$):void 0,u=b?.items??t,s=b?void 0:p,g=Ie(u),S=g.defs,f=S.filter(Oe);L!==null&&!f.some(n=>n.testNumber===L)&&(L=null);const y=e?.keepSections?F.get(I):void 0,x=y??(I==="overview"?Te(u,f,S,s,c):I==="distributions"?ve(u,f,s,c):ke(u,f,s));y||F.set(I,x);const C=g.conflicts.filter(n=>n.excluded);if(C.length>0){const n=l.createElement("div");Object.assign(n.style,{color:B.label,fontSize:G.body,padding:`0 ${W} ${w.sm}`});const a=C.slice(0,6).map(N=>N.testNumber).join(", "),o=C.length>6?`, and ${C.length-6} more`:"",h=C.length===1?"it":"them";n.textContent=`${C.length} test ${C.length===1?"number is":"numbers are"} hidden (${a}${o}) \u2014 the wafers in view describe ${h} differently, so their values cannot be pooled. `+(p&&p.length>0?`Pick one ${c??"group"} under "Show:" to analyse ${h}.`:"Load one test program at a time to analyse them."),n.dataset.wmapWithheldTests=String(C.length),j.appendChild(n)}j.appendChild(x.card)}return{el:O,render:M,destroy:()=>{te(),O.remove()}}}
1
+ import{buildFacetTable as Ye,facetValueOf as Ge,FACET_NONE_VALUE as He}from"../stats/facets.js";import{mergeTestDefs as re}from"../stats/mergeTestDefs.js";import{isParametricTest as Fe}from"../renderer/buildWaferMap.js";import{waferDisplayLabel as Ne,waferIdentityLabel as Ve}from"../core/waferLabel.js";import{hasDrilldownTargets as ie,waferPopulation as _e}from"./chartPopulation.js";import{openDrilldownMenu as Ue}from"./drilldown.js";import{INPUT_DEFAULT_PASS_BINS as R,itemPassBins as ze,passBinsLabel as le}from"../core/passBins.js";import{NO_DATA_FILL as de}from"../renderer/colorMap.js";import{describeWaferPopulation as qe,populationStat as Qe}from"../stats/population.js";import{LEADING as ce,ALPHA as Ke,SPACE as x,EDGE_GUTTER as Z,FONT as W,CLR as k,RADIUS as pe,SHADOW as Xe,controlStyle as ue,wireControlHover as ee,wireTooltip as Je}from"./toolbar.js";import{ICONS as Ze}from"./icons.js";import{renderCapabilityPanel as et}from"./charts/capability.js";import{renderBoxplotPanel as tt}from"./charts/boxplot.js";import{renderTrendPanel as nt}from"./charts/trend.js";import{renderSweepPanel as at}from"./charts/sweep.js";import{sweepAppliesTo as ot}from"../stats/sweep.js";import{renderHistogramPanel as st}from"./charts/histogram.js";import{renderCorrelationPanel as rt}from"./charts/correlation.js";import{renderScatterPanel as it}from"./charts/scatter.js";import{renderBarPanel as fe}from"./charts/barPanel.js";import{renderBinClusterPanel as lt}from"./charts/binCluster.js";import{renderTestPassRatePanel as dt}from"./charts/testPassRate.js";import{QUANTITY as ct}from"./charts/palette.js";import{cardFrameStyle as pt,makeChartGridWrap as V,makeLabeledSelect as ut,makeLinkedGroupSelect as ft}from"./charts/chartShell.js";import{buildYieldData as _,buildYieldDataCombined as me}from"../stats/yield.js";import{buildBinParetoData as mt}from"../stats/binPareto.js";import{buildLotTestSection as bt,buildLotFunctionalSection as gt,buildMetadataStripBox as ht}from"./summaryPanel.js";import{buildRegionYieldData as be,buildRingRegions as yt,buildQuadrantRegions as wt}from"../stats/regions.js";import{renderRegionYieldDiagram as ge}from"./charts/regionYieldDiagram.js";const he=[{key:"overview",label:"Overview"},{key:"distributions",label:"Distributions"},{key:"correlation",label:"Correlation"}],Ct={key:"sweeps",label:"Sweeps"};export function createInsightsTab(v){const{getItems:ye,getLotStats:we,getBinColors:te,getRingCount:Ce,onSaveImage:T,onSaveText:U,openWafer:Y,focusTest:ne}=v,xe=v.showMetadataStrip??!0,j=v.contentInset??Z,p=v.ownerDocument??document,O=p.createElement("div");Object.assign(O.style,{display:"none",flexDirection:"column",width:"100%",paddingTop:"0"});const Se=Z,P=p.createElement("div");Object.assign(P.style,{paddingLeft:Se});const B=p.createElement("div");Object.assign(B.style,{display:"flex",gap:x.xs,alignItems:"center",background:k.menuBg,border:`1px solid ${k.menuBorder}`,borderRadius:pe.container,boxShadow:Xe.panel,padding:`0 ${x.lg}`,marginBottom:x.lg,marginLeft:j,marginRight:j}),B.setAttribute("role","tablist"),B.addEventListener("keydown",e=>{if(e.key!=="ArrowLeft"&&e.key!=="ArrowRight")return;const t=Array.from(B.querySelectorAll('[role="tab"]')),o=t.indexOf(p.activeElement);if(o===-1)return;e.preventDefault();const r=t[(o+(e.key==="ArrowRight"?1:-1)+t.length)%t.length];r.focus(),r.click()});const M=p.createElement("div");Object.assign(M.style,{display:"flex",flexDirection:"column",gap:x.lg,marginTop:"0",paddingLeft:j,paddingRight:j}),O.appendChild(P),O.appendChild(B),O.appendChild(M);const ae=(v.sweeps?.length??0)>0,ve=ae?[...he,Ct]:he;let L=v.defaultView==="sweeps"&&!ae?"overview":v.defaultView??"overview",A,$=null,E=null,G={includeLimits:void 0,clipOutliers:!1};const z=e=>{E!==e&&(E=e,F())};let H=new Map;function oe(){for(const e of H.values())e.destroy();H=new Map}function q(e,t,o){Y?.(e,t,o)}function Q(e){if(e.some(t=>ie(t.testDefs,v.sweeps)))return(t,o,r)=>{const i=e.find(f=>f.waferIndex===t);if(!i||!ie(i.testDefs,v.sweeps))return;r.preventDefault();const l=_e(i.dies,{waferLabel:i.identity,testDefs:i.testDefs,activeTest:o??$??void 0,waferIndex:t});Ue({x:r.clientX,y:r.clientY},r.target,l,{sweeps:v.sweeps,onSaveImage:T})}}function Te(e){return Y?{open:(t,o)=>q(t,`Wafer ${e.find(r=>r.waferIndex===t)?.label??t}`,o)}:ne&&e.length===1?{open:(t,o)=>ne(o),label:"show this test on the map"}:null}function ke(){return ye().map((e,t)=>e==null?null:{metadata:e.wafer.metadata??void 0,dies:e.dies,wafer:e.wafer,label:Ne(e,t),identity:Ve(e),waferIndex:t,passBins:ze(e),statsSummary:e.statsSummary,testDefs:e.testDefs}).filter(e=>e!=null)}function se(e,t){Object.assign(e.style,{background:"none",border:"none",borderBottom:t?`2px solid ${k.iconActive}`:"2px solid transparent",color:t?k.iconActive:k.label,fontWeight:t?"700":"500",fontSize:W.body,padding:`${x.sm} ${Z} ${x.md}`,cursor:"pointer",marginBottom:"-1px"})}function De(e,t){const o=p.createElement("button");o.type="button",o.textContent=t;const r=e===L;return se(o,r),o.dataset.on=r?"true":"false",ee(o,"bare"),o.dataset.wmapInsightsTab=e,o.setAttribute("role","tab"),o.setAttribute("aria-selected",r?"true":"false"),o.tabIndex=r?0:-1,o.addEventListener("click",()=>{L!==e&&(L=e,F({keepSections:!0}))}),o}function Be(e){const t=p.createElement("button");return t.type="button",t.textContent=`\u2039 ${e.label}`,se(t,!1),t.dataset.wmapInsightsBack="1",t.addEventListener("click",e.onBack),t}function $e(e,t,o,r=!0){const i=V(p),l=te();let f="label";const u=o??"group",m=le(e.map(c=>c.passBins)),d=we?.(),g=new Map(d?.lotYieldSeries.map(c=>[c.waferIndex,c.yieldPercent])),C=c=>({...c,label:c.label.replace(/\s*·\s*\d+(\.\d+)?%$/,""),yieldPercent:g.get(c.waferIndex),key:c.waferIndex}),b=e.map(C),h=t?.map(c=>({key:c.key,items:c.items.map(C)})),S=()=>h?me(h,R,f):_(b,R,f),w={title:t?`Yield by ${u} (pass: ${m})`:`Yield by wafer (pass: ${m})`,data:S(),selfControl:{current:f,options:[["yield","Sort: yield"],["label",t?`Sort: ${u}`:"Sort: wafer ID"]],onChange:c=>(f=c,{data:S()})},barColor:()=>ct,valueLabel:c=>`${c.percent.toFixed(1)}%`,reference:c=>{if(c.length<3)return null;const D=c.map(J=>J.percent).sort((J,Me)=>J-Me),I=D.length%2?D[(D.length-1)/2]:(D[D.length/2-1]+D[D.length/2])/2;return{value:I,label:`median ${I.toFixed(1)}%`}},drill:h?{onOpenGroup:c=>{const D=h.find(I=>I.key===c.label)?.items??[];return{data:_(D,R,f),title:`Yield by wafer \u2014 ${u}: ${c.label} (pass: ${m})`}},onBack:()=>({data:S(),title:`Yield by ${u} (pass: ${m})`}),groupLabelText:u}:void 0,onOpen:Y?c=>{typeof c.key=="number"&&q(c.key,`Wafer ${c.label}`)}:void 0,onWaferContextMenu:Q(e),ownerDocument:p};let n=null;if(r&&(n=fe(w,T),n.card.style.minHeight="360px",i.appendChild(n.card)),t){const c=lt({title:"Hard bin pareto",groups:t.map(D=>({key:D.key,items:D.items.map(I=>({dies:I.dies,hardBinCounts:I.statsSummary?.stats.hardBinCounts,softBinCounts:I.statsSummary?.stats.softBinCounts}))})),onSaveImage:T,ownerDocument:p});return c.card.style.minHeight="360px",i.appendChild(c.card),{card:i,destroy:()=>{n?.destroy(),c.destroy()}}}let a="hbin";const s=c=>c===void 0?de:(a==="hbin"?l.hard:l.soft).get(c)??de,y=e.map(c=>({dies:c.dies,hardBinCounts:c.statsSummary?.stats.hardBinCounts,softBinCounts:c.statsSummary?.stats.softBinCounts})),N=()=>mt(y,a),je={title:"Hard bin pareto",data:N(),selfControl:{current:a,options:[["hbin","Hard bins"],["sbin","Soft bins"]],onChange:c=>(a=c,{data:N(),title:`${a==="hbin"?"Hard":"Soft"} bin pareto`})},barColor:c=>s(c.binCode),ownerDocument:p},X=fe(je,T);return X.card.style.minHeight="360px",i.appendChild(X.card),{card:i,destroy:()=>{n?.destroy(),X.destroy()}}}function K(){const e=p.createElement("div");return e.dataset.wmapChartCard="1",Object.assign(e.style,{...pt(),display:"flex",flexDirection:"column",gap:x.lg,minWidth:"0"}),e}function Ee(e){if(P.innerHTML="",P.style.marginBottom="0",!xe)return;const t=ht(e.map(o=>({metadata:o.metadata})),void 0,{facetableOnly:!1});t&&(t.style.marginBottom="0",P.appendChild(t),P.style.marginBottom=x.xs)}function Le(e,t,o,r){if(!e.length)return{elements:[],testValuesCard:null,functionalCard:null,destroy:()=>{}};const i=e.map(s=>s.wafer),l=e.map(s=>s.dies),f=e.flatMap(s=>s.dies),u=e.map(s=>s.statsSummary),m=u.every(s=>s!==void 0)?u:void 0,d=Ce?.()??4,g=[],C=[],h=dt({groups:(r??[{key:"",items:e}]).map(s=>({key:s.key,items:s.items.map(y=>({dies:y.dies,testSpecYield:y.statsSummary?.stats.testSpecYield,functionalYield:y.statsSummary?.stats.functionalYield}))})),testDefs:o,onSaveImage:T,ownerDocument:p});h.card.style.minHeight="300px",g.push(h.card),C.push(h.destroy);const S=be(l,i,d,s=>e[s].passBins,yt);if(S.length){const s=ge({title:"Ring yield",mode:"ring",rows:S,onSaveImage:T,ownerDocument:p});g.push(s.card),C.push(s.destroy)}const w=be(l,i,d,s=>e[s].passBins,wt);if(w.length){const s=ge({title:"Quadrant yield",mode:"quadrant",rows:w,onSaveImage:T,ownerDocument:p});g.push(s.card),C.push(s.destroy)}let n=null;if(t.length){const s=bt(f,t,void 0,m,U,l.map(y=>({dies:y})),void 0,"full");if(s){const y=K();y.appendChild(s),n=y}}let a=null;if(o.length){const s=gt(f,o,m,U);if(s){const y=K();y.appendChild(s),a=y}}return{elements:g,testValuesCard:n,functionalCard:a,destroy:()=>{for(const s of C)s()}}}function Ie(e){const t=le(e.map(d=>d.passBins)),o=e.length===1,r=e[0],i=K();Object.assign(i.style,{flexDirection:"row",flexWrap:"wrap",gap:x.md});const l=d=>{const g=d.lastElementChild;return g&&Object.assign(g.style,{borderRight:"none",paddingRight:"0",marginRight:"0"}),d};function f(d,g,C){const b=p.createElement("div");Object.assign(b.style,{display:"flex",alignItems:"baseline",gap:x.sm,paddingRight:x.xxl,marginRight:x.md,borderRight:`1px solid ${k.menuBorder}`});const h=p.createElement("div");h.textContent=d,Object.assign(h.style,{fontSize:W.heading,fontWeight:"700",color:k.value,lineHeight:ce.tight});const S=p.createElement("div");if(S.textContent=g,Object.assign(S.style,{fontSize:W.body,color:k.label}),b.append(h,S),C){const w=p.createElement("div");w.textContent=C,Object.assign(w.style,{fontSize:W.meta,color:k.label,opacity:Ke.muted}),b.appendChild(w)}return b}if(o){const d=_([{...r,key:r.waferIndex}],R)[0]?.percent;return d!==void 0&&i.appendChild(f(`${d.toFixed(1)}%`,`Yield \xB7 pass: ${t}`)),i.appendChild(f(String(r.dies.length),"Total dies")),l(i)}i.appendChild(f(String(e.length),"Wafers"));const u=_(e.map(d=>({...d,key:d.waferIndex})),R).map(d=>d.percent).filter(d=>Number.isFinite(d));if(u.length){const d=u.reduce((b,h)=>b+h,0)/u.length,g=me([{key:"all",items:e.map(b=>({...b,key:b.waferIndex}))}],R)[0]?.percent,C=g!==void 0&&g.toFixed(1)!==d.toFixed(1);i.appendChild(f(`${d.toFixed(1)}%`,`Mean per-wafer yield \xB7 pass: ${t}`,C?`${g.toFixed(1)}% by dies`:void 0))}const m=e.map(d=>d.statsSummary);if(m.every(d=>d!==void 0)){const d=m.reduce((C,b)=>C+b.stats.analyzedDies,0),g=m.reduce((C,b)=>C+b.stats.excludedDies,0);i.appendChild(f(d.toLocaleString(),"Dies analysed",g?`${g.toLocaleString()} excluded`:void 0))}else i.appendChild(f(e.reduce((d,g)=>d+g.dies.length,0).toLocaleString(),"Total dies"));return l(i)}function Oe(e,t,o,r,i){const l=p.createElement("div");Object.assign(l.style,{display:"flex",flexDirection:"column",gap:x.lg});const f=e.length===1&&!r;e.length&&!r&&l.appendChild(Ie(e));const u=$e(e,r,i,!f);l.appendChild(u.card);const m=Le(e,t,o,r);for(const d of m.elements)u.card.appendChild(d);return m.testValuesCard&&l.appendChild(m.testValuesCard),m.functionalCard&&l.appendChild(m.functionalCard),{card:l,destroy:()=>{u.destroy(),m.destroy()}}}function Pe(e,t,o,r){const i=V(p),l=Te(e),f=a=>{G=a;for(const s of[b,h,S])s!==u&&s?.setAxisPrefs(a)};let u=null;const m=a=>s=>{u=a(),f(s),u=null},d=a=>({...a,testStats:a.statsSummary?.stats.perTestStats}),g=e.map(d),C=o?.map(a=>({key:a.key,items:a.items.map(d)})),b=tt({title:"Test value distribution",axisPrefs:G,onAxisPrefsChange:m(()=>b),onTestChange:a=>w(a),selectedTestNumber:$??void 0,items:g,testDefs:t,groups:C,groupLabelText:r,onSaveImage:T,onGroupChange:a=>z(a),onOpen:l?.open,openActionLabel:l?.label,onWaferContextMenu:Q(e),ownerDocument:p}),h=st({title:"Value histogram",axisPrefs:G,onAxisPrefsChange:m(()=>h),onTestChange:a=>w(a),selectedTestNumber:$??void 0,items:e,testDefs:t,groups:o,onGroupChange:a=>z(a),onSaveImage:T,ownerDocument:p}),S=nt({title:"Wafer-to-wafer trend",axisPrefs:G,onAxisPrefsChange:m(()=>S),onTestChange:a=>w(a),selectedTestNumber:$??void 0,items:e.map(a=>({label:a.label,key:a.waferIndex,dies:a.dies,testStats:a.statsSummary?.stats.perTestStats})),testDefs:t,onSaveImage:T,centreLabel:Qe(qe(e.map(a=>a.wafer.metadata)),"mean"),onOpen:Y?(a,s)=>q(a,`Wafer ${e.find(y=>y.waferIndex===a)?.label??a}`,s):void 0,onWaferContextMenu:Q(e),ownerDocument:p}),w=a=>{$=a,b.setTest(a),h.setTest(a),S.setTest(a),n.setTest(a)},n=et({title:"Process capability",items:e,testDefs:t,onSaveImage:T,selectedTestNumber:$??void 0,onSelectTest:a=>w(a),ownerDocument:p});return i.append(n.card,b.card,h.card,S.card),{card:i,destroy:()=>{n.destroy(),b.destroy(),h.destroy(),S.destroy()}}}function Ae(e){const t=V(p),o=re(e).defs,r=e.flatMap(u=>u.dies),i=v.sweeps??[],l=i.filter(u=>!ot(u,o));l.length>0&&t.appendChild(Re(l,i.length));const f=i.map(u=>at({spec:u,dies:r,testDefs:o,onSaveImage:T,ownerDocument:p}));for(const u of f)t.appendChild(u.card);return{card:t,destroy:()=>{for(const u of f)u.destroy()}}}function Re(e,t){const o=p.createElement("div");o.setAttribute("role","status"),Object.assign(o.style,{gridColumn:"1 / -1",display:"flex",flexWrap:"wrap",alignItems:"center",gap:x.md,padding:`${x.sm} ${x.md}`,background:k.warnBg,border:`1px solid ${k.warnBorder}`,borderRadius:pe.container,color:k.warnText,fontSize:W.body,lineHeight:ce.base});const r=e.length,i=p.createElement("span");if(i.style.flex="1 1 24ch",i.textContent=(r===t?r===1?"This sweep names no test in this data":"None of these sweeps names a test in this data":`${r} of ${t} sweeps name no test in this data: ${e.map(l=>l.title).join(", ")}`)+` \u2014 ${r===1?"it":"they"} may belong to another test program.`,o.appendChild(i),v.onRemoveSweeps){const l=p.createElement("button");l.type="button",Object.assign(l.style,ue("outlined")),l.textContent=r===1?"Remove this sweep":`Remove these ${r} sweeps`,ee(l);const f=e.map(u=>u.id);l.addEventListener("click",()=>v.onRemoveSweeps(f)),o.appendChild(l)}return o}function We(e,t,o){const r=V(p),i=it({title:"Test scatter",items:e,testDefs:t,groups:o,binColors:te().hard,onSaveImage:T,ownerDocument:p}),l=rt({title:"Test correlation matrix",items:e,testDefs:t,onSaveImage:T,onSaveText:U,onSelectPair:(f,u)=>i.setXY(f,u),ownerDocument:p});return r.append(l.card,i.card),{card:r,destroy:()=>{l.destroy(),i.destroy()}}}function F(e){e?.keepSections||oe(),M.innerHTML="",B.innerHTML="",v.backTab&&B.appendChild(Be(v.backTab));for(const n of ve)B.appendChild(De(n.key,n.label));const t=ke();Ee(t);const o=Ye(t,{facetableOnly:!0}).filter(n=>n.splittable),r=p.createElement("div");Object.assign(r.style,{display:"flex",gap:x.md,alignItems:"center",marginLeft:x.xxl,paddingBottom:x.xs});let i;o.length>0?(r.appendChild(ut("Group by:",[{value:"",label:"None"},...o.map(n=>({value:n.key,label:`${n.label} (${n.values.length})`}))],A??"",n=>{A=n||void 0,E=null,F()},{hook:"group-by",ownerDocument:p})),i=o.find(n=>n.key===A)?.label):A=void 0;let l;if(A){const n=new Map,a=[];for(const s of t){const y=Ge(s.metadata,A)??He;n.has(y)||(n.set(y,[]),a.push(y)),n.get(y).push(s)}l=a.map(s=>({key:s,items:n.get(s)}))}E!==null&&!l?.some(n=>n.key===E)&&(E=null),l&&l.length>0&&r.appendChild(ft(l.map(n=>n.key),"Show:",E,n=>z(n),{ownerDocument:p}).el);let f=null;if(v.onOpenGuide){const n=p.createElement("button");n.type="button",n.innerHTML=Ze.help,n.setAttribute("aria-label","User guide"),Object.assign(n.style,{...ue("bare"),display:"inline-flex",alignItems:"center",justifyContent:"center",width:"24px",height:"24px",flexShrink:"0"}),ee(n,"bare"),Je(n,"User guide"),n.addEventListener("click",()=>v.onOpenGuide()),Object.assign(n.style,{marginLeft:"auto"}),f=n}B.appendChild(r),f&&B.appendChild(f);const u=E!==null?l?.find(n=>n.key===E):void 0,m=u?.items??t,d=u?void 0:l,g=re(m),C=g.defs,b=C.filter(Fe);$!==null&&!b.some(n=>n.testNumber===$)&&($=null);const h=e?.keepSections?H.get(L):void 0,S=h??(L==="overview"?Oe(m,b,C,d,i):L==="distributions"?Pe(m,b,d,i):L==="sweeps"?Ae(m):We(m,b,d));h||H.set(L,S);const w=g.conflicts.filter(n=>n.excluded);if(w.length>0){const n=p.createElement("div");Object.assign(n.style,{color:k.label,fontSize:W.body,padding:`0 ${j} ${x.sm}`});const a=w.slice(0,6).map(N=>N.testNumber).join(", "),s=w.length>6?`, and ${w.length-6} more`:"",y=w.length===1?"it":"them";n.textContent=`${w.length} test ${w.length===1?"number is":"numbers are"} hidden (${a}${s}) \u2014 the wafers in view describe ${y} differently, so their values cannot be pooled. `+(l&&l.length>0?`Pick one ${i??"group"} under "Show:" to analyse ${y}.`:"Load one test program at a time to analyse them."),n.dataset.wmapWithheldTests=String(w.length),M.appendChild(n)}M.appendChild(S.card)}return{el:O,render:F,destroy:()=>{oe(),O.remove()}}}
@@ -1 +1 @@
1
- import{getDieTestValue as $}from"../renderer/buildWaferMap.js";import{resolveTestNumber as T,findTestDef as B}from"../renderer/buildView.js";import{buildBinSection as V}from"./summaryPanel.js";import{buildTestHistogramData as z}from"../stats/histogram.js";import{resolveValueColorFn as F}from"../renderer/colorSchemes.js";import{TRACKING as N,SPACE as y,RADIUS as I,FONT as S,CLR as u,wireTooltip as A}from"./toolbar.js";import{fmt as v}from"../renderer/fmt.js";function d(o,n,t){const a=document.createElement(o);return n&&Object.assign(a.style,n),t!==void 0&&(a.textContent=t),a}function W(o,n,t,a,x){let p=1/0,m=-1/0;for(const r of o){const s=$(r,n);s!==void 0&&(s<p&&(p=s),s>m&&(m=s))}const b=a==="spec"&&t&&(t.limitLow!==void 0||t.limitHigh!==void 0);let e,l;b?(e=t.limitLow!==void 0?t.limitLow:isFinite(p)?p:0,l=t.limitHigh!==void 0?t.limitHigh:isFinite(m)?m:1):(e=isFinite(p)?p:0,l=isFinite(m)?m:1),e===l&&(l=e+1);const h=r=>Math.min(1,Math.max(0,r));if(x&&e>0&&l>0){const r=Math.log10(e),s=Math.log10(l)-r;return c=>c<=0?0:h((Math.log10(c)-r)/s)}return r=>h((r-e)/(l-e))}function E(o,n,t,a,x,p,m,b){const e=d("div",{display:"flex",flexDirection:"column",gap:y.xxs,minWidth:"0",flex:"1",minHeight:"0"});e.appendChild(d("div",{fontSize:S.meta,fontWeight:"700",letterSpacing:N,textTransform:"uppercase",color:u.label},n?.name?`${n.name} distribution`:"Value distribution"));const l=o[0].rangeLow,h=o[o.length-1].rangeHigh,r=h-l||1,s=i=>Math.min(1,Math.max(0,(i-l)/r)),c=F(a,x),f=W(p,t,n,m,b),w=Math.max(1,...o.map(i=>i.count)),L=d("div",{position:"relative",display:"flex",alignItems:"flex-end",gap:y.xxs,flex:"1",minHeight:"48px"});for(const i of o){const g=d("div",{flex:"1",minWidth:"2px",background:c(f((i.rangeLow+i.rangeHigh)/2)),height:`${i.count/w*100}%`,borderRadius:"1px 1px 0 0"});g.dataset.wmapBar="1",A(g,`${v(i.rangeLow,n?.unit)} \u2013 ${v(i.rangeHigh,n?.unit)}: ${i.count}`,{asDataPoint:!0}),L.appendChild(g)}const H=[];for(const[i,g]of[[n?.limitLow,"LSL"],[n?.limitHigh,"USL"]]){if(i===void 0||i<l||i>h)continue;const C=s(i)*100,M=d("div",{position:"absolute",top:"0",bottom:"0",left:`${C}%`,width:"2px",marginLeft:"-1px",background:u.value,boxShadow:`0 0 0 1px ${u.panelBg}`,pointerEvents:"none"});M.dataset.wmapLimitMarker="1",L.appendChild(M),H.push({pct:C,text:`${g} ${v(i,n?.unit)}`})}if(e.appendChild(L),H.length){const i=d("div",{position:"relative",height:"14px"});for(const{pct:g,text:C}of H){const M=Math.min(96,Math.max(4,g));i.appendChild(d("span",{position:"absolute",left:`${M}%`,top:"0",transform:"translateX(-50%)",whiteSpace:"nowrap",fontSize:S.body,fontWeight:"600",color:u.text,background:u.panelBg,border:`1px solid ${u.value}`,borderRadius:I.control,padding:"0 3px",lineHeight:"12px"},C))}e.appendChild(i)}const R=d("div",{display:"flex",justifyContent:"space-between",fontSize:S.body,color:u.label});return R.appendChild(d("span",{},v(o[0].rangeLow,n?.unit))),R.appendChild(d("span",{},v(o[o.length-1].rangeHigh,n?.unit))),e.appendChild(R),e}export function buildMaplessSummary(o,n,t){const{plotMode:a,activeTest:x,hbinDefs:p,sbinDefs:m,binColors:b,valueColorScheme:e,reverseValueScheme:l,logScale:h=!1,colorbarRangeMode:r="spec",valueRangeDies:s}=t;if(a==="hardBin"||a==="softBin"){const f=V(o,a==="hardBin"?p:m,a==="hardBin"?"hard":"soft",b);if(f)return f}else if(a==="value"){const{testNumber:c}=T(x??0,n),f=B(n,x??0),w=z([{dies:o}],c,12,f?.limitLow,f?.limitHigh);if(w.length)return E(w,f,c,e,l,s??o,r,h)}return d("div",{padding:y.xl,fontSize:S.body,color:u.label,textAlign:"center"},"No summary available for this view.")}
1
+ import{getDieTestValue as $}from"../renderer/buildWaferMap.js";import{resolveTestNumber as T,findTestDef as B}from"../renderer/buildView.js";import{buildBinSection as V}from"./summaryPanel.js";import{buildTestHistogramData as z}from"../stats/histogram.js";import{resolveValueColorFn as F}from"../renderer/colorSchemes.js";import{TRACKING as N,SPACE as y,RADIUS as I,FONT as H,CLR as u,wireTooltip as A}from"./toolbar.js";import{fmt as v}from"../renderer/fmt.js";function d(o,n,e){const a=document.createElement(o);return n&&Object.assign(a.style,n),e!==void 0&&(a.textContent=e),a}function W(o,n,e,a,x){let p=1/0,m=-1/0;for(const r of o){const s=$(r,n);s!==void 0&&(s<p&&(p=s),s>m&&(m=s))}const b=a==="spec"&&e&&(e.limitLow!==void 0||e.limitHigh!==void 0);let t,l;b?(t=e.limitLow!==void 0?e.limitLow:isFinite(p)?p:0,l=e.limitHigh!==void 0?e.limitHigh:isFinite(m)?m:1):(t=isFinite(p)?p:0,l=isFinite(m)?m:1),t===l&&(l=t+1);const h=r=>Math.min(1,Math.max(0,r));if(x&&t>0&&l>0){const r=Math.log10(t),s=Math.log10(l)-r;return c=>c<=0?0:h((Math.log10(c)-r)/s)}return r=>h((r-t)/(l-t))}function E(o,n,e,a,x,p,m,b){const t=d("div",{display:"flex",flexDirection:"column",gap:y.xxs,minWidth:"0",flex:"1",minHeight:"0"});t.appendChild(d("div",{fontSize:H.meta,fontWeight:"700",letterSpacing:N,textTransform:"uppercase",color:u.label},n?.name?`${n.name} distribution`:"Value distribution"));const l=o[0].rangeLow,h=o[o.length-1].rangeHigh,r=h-l||1,s=i=>Math.min(1,Math.max(0,(i-l)/r)),c=F(a,x),f=W(p,e,n,m,b),w=Math.max(1,...o.map(i=>i.count)),S=d("div",{position:"relative",display:"flex",alignItems:"flex-end",gap:y.xxs,flex:"1",minHeight:"48px"});for(const i of o){const g=d("div",{flex:"1",minWidth:"2px",background:c(f((i.rangeLow+i.rangeHigh)/2)),height:`${i.count/w*100}%`,borderRadius:"1px 1px 0 0"});g.dataset.wmapBar="1",A(g,`${v(i.rangeLow,n?.unit)} \u2013 ${v(i.rangeHigh,n?.unit)}: ${i.count}`,{asDataPoint:!0}),S.appendChild(g)}const L=[];for(const[i,g]of[[n?.limitLow,"Lo limit"],[n?.limitHigh,"Hi limit"]]){if(i===void 0||i<l||i>h)continue;const C=s(i)*100,M=d("div",{position:"absolute",top:"0",bottom:"0",left:`${C}%`,width:"2px",marginLeft:"-1px",background:u.value,boxShadow:`0 0 0 1px ${u.panelBg}`,pointerEvents:"none"});M.dataset.wmapLimitMarker="1",S.appendChild(M),L.push({pct:C,text:`${g} ${v(i,n?.unit)}`})}if(t.appendChild(S),L.length){const i=d("div",{position:"relative",height:"14px"});for(const{pct:g,text:C}of L){const M=Math.min(96,Math.max(4,g));i.appendChild(d("span",{position:"absolute",left:`${M}%`,top:"0",transform:"translateX(-50%)",whiteSpace:"nowrap",fontSize:H.body,fontWeight:"600",color:u.text,background:u.panelBg,border:`1px solid ${u.value}`,borderRadius:I.control,padding:"0 3px",lineHeight:"12px"},C))}t.appendChild(i)}const R=d("div",{display:"flex",justifyContent:"space-between",fontSize:H.body,color:u.label});return R.appendChild(d("span",{},v(o[0].rangeLow,n?.unit))),R.appendChild(d("span",{},v(o[o.length-1].rangeHigh,n?.unit))),t.appendChild(R),t}export function buildMaplessSummary(o,n,e){const{plotMode:a,activeTest:x,hbinDefs:p,sbinDefs:m,binColors:b,valueColorScheme:t,reverseValueScheme:l,logScale:h=!1,colorbarRangeMode:r="spec",valueRangeDies:s}=e;if(a==="hardBin"||a==="softBin"){const f=V(o,a==="hardBin"?p:m,a==="hardBin"?"hard":"soft",b);if(f)return f}else if(a==="value"){const{testNumber:c}=T(x??0,n),f=B(n,x??0),w=z([{dies:o}],c,12,f?.limitLow,f?.limitHigh);if(w.length)return E(w,f,c,t,l,s??o,r,h)}return d("div",{padding:y.xl,fontSize:H.body,color:u.label,textAlign:"center"},"No summary available for this view.")}