@wafertools/wafermap 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/AGENTS.md +6 -6
  2. package/CHANGELOG.md +212 -0
  3. package/README.md +5 -5
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +15 -10
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +7 -0
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/binCluster.d.ts +7 -1
  9. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +55 -3
  13. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/correlation.js +1 -2
  15. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +2 -0
  16. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/histogram.d.ts +16 -0
  18. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  19. package/dist/packages/canvas-adapter/charts/palette.d.ts +2 -0
  20. package/dist/packages/canvas-adapter/charts/palette.js +1 -1
  21. package/dist/packages/canvas-adapter/charts/plotChart.d.ts +27 -0
  22. package/dist/packages/canvas-adapter/charts/plotChart.js +1 -0
  23. package/dist/packages/canvas-adapter/charts/pointInteractions.d.ts +45 -0
  24. package/dist/packages/canvas-adapter/charts/pointInteractions.js +1 -0
  25. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +2 -0
  26. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  27. package/dist/packages/canvas-adapter/charts/scatter.d.ts +19 -0
  28. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  29. package/dist/packages/canvas-adapter/charts/sweep.d.ts +12 -0
  30. package/dist/packages/canvas-adapter/charts/sweep.js +1 -1
  31. package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +7 -0
  32. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  33. package/dist/packages/canvas-adapter/charts/trend.d.ts +11 -0
  34. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  35. package/dist/packages/canvas-adapter/confirmDialog.d.ts +13 -0
  36. package/dist/packages/canvas-adapter/confirmDialog.js +1 -0
  37. package/dist/packages/canvas-adapter/dataTab.d.ts +131 -0
  38. package/dist/packages/canvas-adapter/dataTab.js +1 -0
  39. package/dist/packages/canvas-adapter/dieList.d.ts +39 -4
  40. package/dist/packages/canvas-adapter/dieList.js +6 -7
  41. package/dist/packages/canvas-adapter/drilldown.d.ts +20 -3
  42. package/dist/packages/canvas-adapter/drilldown.js +1 -1
  43. package/dist/packages/canvas-adapter/guideWindow.d.ts +96 -0
  44. package/dist/packages/canvas-adapter/guideWindow.js +1 -0
  45. package/dist/packages/canvas-adapter/identityHeader.d.ts +16 -0
  46. package/dist/packages/canvas-adapter/identityHeader.js +1 -1
  47. package/dist/packages/canvas-adapter/index.d.ts +3 -3
  48. package/dist/packages/canvas-adapter/index.js +1 -1
  49. package/dist/packages/canvas-adapter/insightsTab.d.ts +73 -54
  50. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  51. package/dist/packages/canvas-adapter/maplessSummary.d.ts +2 -2
  52. package/dist/packages/canvas-adapter/plotEditor.d.ts +29 -0
  53. package/dist/packages/canvas-adapter/plotEditor.js +1 -0
  54. package/dist/packages/canvas-adapter/plotItems.d.ts +32 -0
  55. package/dist/packages/canvas-adapter/plotItems.js +1 -0
  56. package/dist/packages/canvas-adapter/plotModal.d.ts +30 -0
  57. package/dist/packages/canvas-adapter/plotModal.js +1 -0
  58. package/dist/packages/canvas-adapter/plotStore.d.ts +52 -0
  59. package/dist/packages/canvas-adapter/plotStore.js +1 -0
  60. package/dist/packages/canvas-adapter/plotTab.d.ts +30 -0
  61. package/dist/packages/canvas-adapter/plotTab.js +1 -0
  62. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +3 -2
  63. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  64. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +21 -2
  65. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  66. package/dist/packages/canvas-adapter/summaryPanel.d.ts +27 -8
  67. package/dist/packages/canvas-adapter/summaryPanel.js +2 -4
  68. package/dist/packages/canvas-adapter/summaryRail.d.ts +7 -0
  69. package/dist/packages/canvas-adapter/summaryRail.js +1 -0
  70. package/dist/packages/canvas-adapter/sweepEditor.d.ts +17 -0
  71. package/dist/packages/canvas-adapter/sweepEditor.js +1 -0
  72. package/dist/packages/canvas-adapter/tableExport.d.ts +22 -0
  73. package/dist/packages/canvas-adapter/tableExport.js +3 -0
  74. package/dist/packages/canvas-adapter/toolbar.d.ts +25 -97
  75. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  76. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  77. package/dist/packages/canvas-adapter/userGuideHtml.js +337 -42
  78. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  79. package/dist/packages/canvas-adapter/version.js +1 -1
  80. package/dist/packages/canvas-adapter/viewSwitch.d.ts +13 -0
  81. package/dist/packages/canvas-adapter/viewSwitch.js +1 -0
  82. package/dist/packages/canvas-adapter/virtualTable.d.ts +44 -0
  83. package/dist/packages/canvas-adapter/virtualTable.js +1 -0
  84. package/dist/packages/core/classify.d.ts +3 -2
  85. package/dist/packages/core/classify.js +1 -1
  86. package/dist/packages/core/passBins.d.ts +17 -4
  87. package/dist/packages/core/passBins.js +1 -1
  88. package/dist/packages/core/ringCount.d.ts +22 -0
  89. package/dist/packages/core/ringCount.js +1 -0
  90. package/dist/packages/core/tableCsv.d.ts +43 -0
  91. package/dist/packages/core/tableCsv.js +3 -0
  92. package/dist/packages/renderer/binColors.d.ts +6 -6
  93. package/dist/packages/renderer/binColors.js +1 -1
  94. package/dist/packages/renderer/buildView.d.ts +1 -1
  95. package/dist/packages/renderer/buildView.js +1 -1
  96. package/dist/packages/renderer/buildWaferMap.js +1 -1
  97. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  98. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  99. package/dist/packages/stats/binPareto.d.ts +1 -1
  100. package/dist/packages/stats/binPareto.js +1 -1
  101. package/dist/packages/stats/histogram.d.ts +27 -0
  102. package/dist/packages/stats/histogram.js +1 -1
  103. package/dist/packages/stats/index.d.ts +2 -0
  104. package/dist/packages/stats/index.js +1 -1
  105. package/dist/packages/stats/lotDrift.js +1 -1
  106. package/dist/packages/stats/lotPattern.js +1 -1
  107. package/dist/packages/stats/math.d.ts +9 -6
  108. package/dist/packages/stats/math.js +1 -1
  109. package/dist/packages/stats/patternClassification.d.ts +1 -1
  110. package/dist/packages/stats/patternClassification.js +1 -1
  111. package/dist/packages/stats/plotData.d.ts +258 -0
  112. package/dist/packages/stats/plotData.js +1 -0
  113. package/dist/packages/stats/plotId.d.ts +3 -0
  114. package/dist/packages/stats/plotId.js +1 -0
  115. package/dist/packages/stats/plotSpec.d.ts +119 -0
  116. package/dist/packages/stats/plotSpec.js +4 -0
  117. package/dist/packages/stats/presentation.d.ts +2 -0
  118. package/dist/packages/stats/presentation.js +1 -1
  119. package/dist/packages/stats/regions.d.ts +6 -0
  120. package/dist/packages/stats/regions.js +1 -1
  121. package/dist/packages/stats/renderSummaryReport.d.ts +8 -3
  122. package/dist/packages/stats/renderSummaryReport.js +15 -15
  123. package/dist/packages/stats/reportHtml.d.ts +1 -1
  124. package/dist/packages/stats/reportHtml.js +36 -29
  125. package/dist/packages/stats/scatter.d.ts +6 -0
  126. package/dist/packages/stats/scatter.js +1 -1
  127. package/dist/packages/stats/sweep.d.ts +37 -1
  128. package/dist/packages/stats/sweep.js +1 -1
  129. package/dist/packages/stats/sweepText.d.ts +14 -0
  130. package/dist/packages/stats/sweepText.js +1 -0
  131. package/dist/packages/stats/sweepXFromName.d.ts +14 -0
  132. package/dist/packages/stats/sweepXFromName.js +1 -1
  133. package/dist/packages/stats/synthesis.d.ts +14 -4
  134. package/dist/packages/stats/synthesis.js +1 -1
  135. package/dist/packages/stats/testPassRate.d.ts +6 -0
  136. package/dist/packages/stats/testPassRate.js +1 -1
  137. package/dist/packages/stats/trend.js +1 -1
  138. package/dist/packages/stats/types.d.ts +1 -1
  139. package/dist/packages/stats/yield.d.ts +15 -6
  140. package/dist/packages/stats/yield.js +1 -1
  141. package/llms.txt +15 -3
  142. package/package.json +3 -3
@@ -1 +1 @@
1
- import{parseRegionKey as A}from"../../stats/regions.js";import{yieldFill as D}from"./palette.js";import{fontPx as C}from"../toolbar.js";import{cardShell as N,resolveChartCanvasColors as F,observeResize as $,growCardToFitContent as j,setChartGrow as q,isExpandedCard as z,bodyRoom as G,renderEmptyState as O,prepareCanvas as _}from"./chartShell.js";const v=320,L=220,Q={SE:[0,Math.PI/2],SW:[Math.PI/2,Math.PI],NW:[Math.PI,1.5*Math.PI],NE:[1.5*Math.PI,2*Math.PI]};function Y(e,a,o,r,l,n){e.beginPath(),e.moveTo(a+n,o),e.arcTo(a+r,o,a+r,o+l,n),e.arcTo(a+r,o+l,a,o+l,n),e.arcTo(a,o+l,a,o,n),e.arcTo(a,o,a+r,o,n),e.closePath()}function w(e,a,o,r,l,n){e.font=`${C(-1)}px system-ui, sans-serif`;const c=e.measureText(r).width;e.font="700 11px system-ui, sans-serif";const y=e.measureText(l).width,m=Math.max(c,y)+12,g=30;e.fillStyle=n.bg,Y(e,a-m/2,o-g/2,m,g,4),e.fill(),e.strokeStyle=n.border,e.lineWidth=1,e.stroke(),e.textAlign="center",e.textBaseline="middle",e.fillStyle=n.textMuted,e.font=`${C(-1)}px system-ui, sans-serif`,e.fillText(r,a,o-7),e.fillStyle=n.text,e.font="700 11px system-ui, sans-serif",e.fillText(l,a,o+7)}export function renderRegionYieldDiagram(e){const{mode:a,rows:o,onSaveImage:r}=e,l=e.title??(a==="ring"?"Ring yield":"Quadrant yield"),{card:n,body:c}=N(l,r,e.ownerDocument);q(n,"square");const y=o.length>0;if(!y)return O(c,"No bin data available for regional yield."),{card:n,hasData:y,destroy:()=>{}};Object.assign(c.style,{display:"flex",justifyContent:"center",alignItems:"center"});const m=n.ownerDocument.createElement("canvas");Object.assign(m.style,{display:"block",flex:"none"}),c.appendChild(m);function g(){const S=Math.max(L,Math.min(c.clientWidth||v,v));j(n,c,S);const b=z(n)?Math.max(S,Math.floor(Math.min(c.clientWidth,G(n,c,m)))):S,k=_(m,n,b,b);if(!k)return;const{ctx:t}=k,u=F(n),T=D,d=b/2,h=b/2,f=b/2-34;if(a==="ring"){const i=o.length;for(let s=0;s<i;s++){const M=s/i*f,P=(s+1)/i*f;t.beginPath(),t.arc(d,h,P,0,2*Math.PI),t.arc(d,h,M,0,2*Math.PI,!0),t.closePath(),t.fillStyle=T(o[s].yieldPercent),t.fill(),t.strokeStyle=u.border,t.lineWidth=1,t.stroke()}t.beginPath(),t.arc(d,h,f,0,2*Math.PI),t.strokeStyle=u.text,t.lineWidth=1.5,t.stroke();for(let s=0;s<i;s++){const M=(s+.5)/i*f,P=-Math.PI/2+s/i*2*Math.PI,I=d+Math.cos(P)*M,p=h+Math.sin(P)*M;w(t,I,p,o[s].label,`${o[s].yieldPercent.toFixed(1)}%`,u)}}else{for(const i of o){const s=A(i.key).quadrant,M=s?Q[s]:void 0;if(!M)continue;const[P,I]=M;t.beginPath(),t.moveTo(d,h),t.arc(d,h,f,P,I),t.closePath(),t.fillStyle=T(i.yieldPercent),t.fill(),t.strokeStyle=u.border,t.lineWidth=1,t.stroke();const p=(P+I)/2,W=d+Math.cos(p)*f*.6,E=h+Math.sin(p)*f*.6;w(t,W,E,i.label,`${i.yieldPercent.toFixed(1)}%`,u)}t.beginPath(),t.arc(d,h,f,0,2*Math.PI),t.strokeStyle=u.text,t.lineWidth=1.5,t.stroke()}}const R=$(n,()=>g());return g(),{card:n,hasData:y,destroy:()=>R.disconnect()}}
1
+ import{parseRegionKey as T}from"../../stats/regions.js";import{yieldFill as q}from"./palette.js";import{fontPx as v}from"../toolbar.js";import{cardShell as $,resolveChartCanvasColors as j,observeResize as z,growCardToFitContent as G,setChartGrow as L,isExpandedCard as O,bodyRoom as Y,renderEmptyState as _,prepareCanvas as B}from"./chartShell.js";const w=320,Q=220,E={SE:[0,Math.PI/2],SW:[Math.PI/2,Math.PI],NW:[Math.PI,1.5*Math.PI],NE:[1.5*Math.PI,2*Math.PI]};function X(e,a,n,h,f,o){e.beginPath(),e.moveTo(a+o,n),e.arcTo(a+h,n,a+h,n+f,o),e.arcTo(a+h,n+f,a,n+f,o),e.arcTo(a,n+f,a,n,o),e.arcTo(a,n,a+h,n,o),e.closePath()}function W(e,a,n,h,f,o){e.font=`${v(-1)}px system-ui, sans-serif`;const u=e.measureText(h).width;e.font="700 11px system-ui, sans-serif";const p=e.measureText(f).width,c=Math.max(u,p)+12,y=30;e.fillStyle=o.bg,X(e,a-c/2,n-y/2,c,y,4),e.fill(),e.strokeStyle=o.border,e.lineWidth=1,e.stroke(),e.textAlign="center",e.textBaseline="middle",e.fillStyle=o.textMuted,e.font=`${v(-1)}px system-ui, sans-serif`,e.fillText(h,a,n-7),e.fillStyle=o.text,e.font="700 11px system-ui, sans-serif",e.fillText(f,a,n+7)}export function renderRegionYieldDiagram(e){const{mode:a,rows:n,onSaveImage:h}=e,f=e.title??(a==="ring"?"Ring yield":"Quadrant yield"),{card:o,body:u}=$(f,h,e.ownerDocument);L(o,"square");const p=n.length>0;if(!p)return _(u,"No bin data available for regional yield."),{card:o,hasData:p,destroy:()=>{}};Object.assign(u.style,{display:"flex",justifyContent:"center",alignItems:"center"});const c=o.ownerDocument.createElement("canvas");Object.assign(c.style,{display:"block",flex:"none"}),u.appendChild(c);let y={cx:0,cy:0,R:0};function C(){const g=Math.max(Q,Math.min(u.clientWidth||w,w));G(o,u,g);const d=O(o)?Math.max(g,Math.floor(Math.min(u.clientWidth,Y(o,u,c)))):g,P=B(c,o,d,d);if(!P)return;const{ctx:t}=P,m=j(o),S=q,s=d/2,i=d/2,M=d/2-34;if(y={cx:s,cy:i,R:M},a==="ring"){const l=n.length;for(let r=0;r<l;r++){const b=r/l*M,I=(r+1)/l*M;t.beginPath(),t.arc(s,i,I,0,2*Math.PI),t.arc(s,i,b,0,2*Math.PI,!0),t.closePath(),t.fillStyle=S(n[r].yieldPercent),t.fill(),t.strokeStyle=m.border,t.lineWidth=1,t.stroke()}t.beginPath(),t.arc(s,i,M,0,2*Math.PI),t.strokeStyle=m.text,t.lineWidth=1.5,t.stroke();for(let r=0;r<l;r++){const b=(r+.5)/l*M,I=-Math.PI/2+r/l*2*Math.PI,R=s+Math.cos(I)*b,k=i+Math.sin(I)*b;W(t,R,k,n[r].label,`${n[r].yieldPercent.toFixed(1)}%`,m)}}else{for(const l of n){const r=T(l.key).quadrant,b=r?E[r]:void 0;if(!b)continue;const[I,R]=b;t.beginPath(),t.moveTo(s,i),t.arc(s,i,M,I,R),t.closePath(),t.fillStyle=S(l.yieldPercent),t.fill(),t.strokeStyle=m.border,t.lineWidth=1,t.stroke();const k=(I+R)/2,N=s+Math.cos(k)*M*.6,F=i+Math.sin(k)*M*.6;W(t,N,F,l.label,`${l.yieldPercent.toFixed(1)}%`,m)}t.beginPath(),t.arc(s,i,M,0,2*Math.PI),t.strokeStyle=m.text,t.lineWidth=1.5,t.stroke()}}function A(g,d){const P=g-y.cx,t=d-y.cy,m=Math.hypot(P,t);if(!(m<=y.R))return null;if(a==="ring")return n[Math.min(n.length-1,Math.floor(m/y.R*n.length))]??null;const S=(Math.atan2(t,P)+2*Math.PI)%(2*Math.PI);return n.find(s=>{const i=E[T(s.key).quadrant??""];return!!i&&S>=i[0]&&S<i[1]})??null}e.onSelectRegion&&(c.style.cursor="pointer",c.addEventListener("click",g=>{const d=c.getBoundingClientRect(),P=A(g.clientX-d.left,g.clientY-d.top);P&&e.onSelectRegion(P,g)}));const D=z(o,()=>C());return C(),{card:o,hasData:p,destroy:()=>D.disconnect()}}
@@ -42,6 +42,25 @@ export interface ScatterPanelOptions {
42
42
  axisPrefs?: AxisPrefs;
43
43
  /** Called when the user changes the "Limits:" choice here. */
44
44
  onAxisPrefsChange?: (prefs: AxisPrefs) => void;
45
+ /**
46
+ * Click a point to open its die's wafer, on the X test — the same
47
+ * `(waferIndex, testNumber)` the boxplot and trend use. Needs each item's
48
+ * `waferIndex`; a point without one is not clickable. Omit for no click action.
49
+ */
50
+ onOpen?: (waferIndex: number, testNumber: number) => void;
51
+ /** What a click does, in the reader's words, when it is not "open this wafer" (a single-wafer host
52
+ * shows the X test on its map instead). Default `open this wafer`. */
53
+ openActionLabel?: string;
54
+ /**
55
+ * Drag a rectangle over the plot to select the dies inside it (only points the
56
+ * legend filter leaves visible). Called on release with those points, the
57
+ * pointer position and the canvas, so the host can open a menu there. The
58
+ * selection stays ringed until X/Y change or a click on empty space.
59
+ */
60
+ onSelect?: (points: ScatterPoint[], at: {
61
+ x: number;
62
+ y: number;
63
+ }, anchor: HTMLElement, xTestNumber: number) => void;
45
64
  /** Document to build this panel's DOM into. Default `document` — pass the
46
65
  * host's own `ownerDocument` when the container might live in a
47
66
  * different document (e.g. a gallery card detached into its own popup
@@ -1 +1 @@
1
- import{NO_DATA_FILL as Ce}from"../../renderer/colorMap.js";import{maxOf as J,minOf as Le}from"../../core/utils.js";import{categorical as Xe}from"./palette.js";import{buildScatterData as Ye,buildScatterDataGrouped as Ke}from"../../stats/scatter.js";import{pearsonOfPairs as Ve}from"../../stats/correlation.js";import{buildFacetTable as qe}from"../../stats/facets.js";import{SPACE as k,RADIUS as Je,fontPx as L,FONT as re,CLR as X}from"../toolbar.js";import{fitTicks as Ae}from"../../renderer/axisTicks.js";import{cardShell as Qe,observeResize as Ze,makeTooltip as et,attachChartTip as tt,makeTestSelect as nt,makeWaferSelect as it,chartFillHeight as ot,applyCanvasFlow as st,drawAxisUnit as lt,resolveChartCanvasColors as at,makeAxisFormat as Q,horizontalTickSpacing as rt,VERTICAL_TICK_SPACING_PX as ct,makeSeriesLegendItem as ft,prepareCanvas as dt,drawOffAxisLimits as Oe,limitLines as mt,hasBothLimitKinds as ut,makeLimitsSelect as pt,stackLabelRows as Pe,strokeLimitLine as De,fillTextOnHalo as Be}from"./chartShell.js";const c=52,xt=16,l=16,ht=44;export const NO_BIN_CATEGORY="none";export function binCategoryOf(h){return h===void 0?NO_BIN_CATEGORY:String(h)}export function binCategories(h){const Y=new Set;let S=!1;for(const O of h)O.hbin===void 0?S=!0:Y.add(O.hbin);const A=Array.from(Y).sort((O,Z)=>O-Z).map(String);return S&&A.push(NO_BIN_CATEGORY),A}export function renderScatterPanel(h){const{title:Y="Test scatter",items:S,testDefs:A,binColors:O,onSaveImage:Z,groups:K}=h,{card:s,body:P,controlsRow:ee}=Qe(Y,Z,h.ownerDocument),V=A.filter(e=>e.testNumber!==void 0),v=!!(K&&K.length>0),ce=(K??[]).map(e=>e.key),Ee=new Map(ce.map((e,n)=>[e,n]));let p=h.xTestNumber??V[0]?.testNumber??null,b=h.yTestNumber??V[1]?.testNumber??p,R=null;const fe=()=>R!==null?S[R]?[S[R]]:[]:S;function de(e,n,o){const r=s.ownerDocument.createElement("label");Object.assign(r.style,{display:"inline-flex",alignItems:"center",gap:k.xs,fontSize:re.body,color:X.label});const T=s.ownerDocument.createElement("span");T.textContent=e;const d=nt(V,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:s.ownerDocument});return r.append(T,d),{wrap:r,select:d}}const{wrap:$e,select:Re}=de("X:",p,e=>{p=e,F()}),{wrap:Ie,select:We}=de("Y:",b,e=>{b=e,F()});ee.append($e,Ie);let M=h.axisPrefs??{clipOutliers:!1};const me=s.ownerDocument.createElement("span");ee.appendChild(me);function ue(){const e=[p,b].map(n=>A.find(o=>o.testNumber===n));me.replaceChildren(...e.some(ut)?[pt(M.limits??"both",n=>{M={...M,limits:n},h.onAxisPrefsChange?.(M),E()},s.ownerDocument)]:[])}!v&&S.length>1&&ee.appendChild(it(S,R,e=>{R=e,F()},{ownerDocument:s.ownerDocument}));const I=s.ownerDocument.createElement("div");Object.assign(I.style,{color:X.warnText,background:X.warnBg,border:`1px solid ${X.warnBorder}`,borderRadius:Je.control,padding:`${k.xs} ${k.md}`,fontSize:re.body,marginBottom:k.xs,display:"none"}),s.insertBefore(I,P);function pe(){const e=v?[]:qe(fe(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);e.length>0?(I.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.`,I.style.display=""):I.style.display="none"}pe();const te=s.ownerDocument.createElement("div");Object.assign(te.style,{color:X.label,fontSize:re.body,marginBottom:k.xs}),s.insertBefore(te,P);function ne(){const e=v?"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=g.size===0?x:x.filter(T=>g.has(q(T))),{r:o,n:r}=Ve(n);te.textContent=o===null?`${e} \xB7 n = ${r.toLocaleString()}`:`${e} \xB7 r = ${o.toFixed(3)} \xB7 n = ${r.toLocaleString()}`}const q=e=>v?e.group??"\u2014":binCategoryOf(e.hbin),xe=e=>v?Xe(Ee.get(e)??0):e===NO_BIN_CATEGORY?Ce:O?.get(Number(e))??Ce,he=e=>v?e:e===NO_BIN_CATEGORY?"No bin data":`Bin ${e}`,g=new Set,W=s.ownerDocument.createElement("div");Object.assign(W.style,{display:"flex",flexWrap:"wrap",gap:k.sm,marginTop:k.md,marginBottom:k.xl}),P.appendChild(W);const D=s.ownerDocument.createElement("canvas");D.style.display="block",D.style.cursor="crosshair",P.appendChild(D);const Ne=et(s);let x=[],y=0,C=1,w=0,B=1;function N(e){const n=A.find(o=>o.testNumber===e);return{unit:n?.unit,lines:mt(n,M.limits)}}function Fe(){const e=P.clientWidth,n=Math.max(200,Math.min(400,e*.65)),o=ot(s,P,D,n);return{w:e,h:o,plotW:Math.max(10,e-c-xt),plotH:Math.max(10,o-l-ht)}}const ie=new Map;function be(){for(const[e,n]of ie){const o=g.has(e);n.setState({selected:o,dimmed:g.size>0&&!o})}}function ge(e){W.innerHTML="",ie.clear(),g.clear();for(const n of e){const o=ft(he(n),xe(n),s.ownerDocument),r=o.el;r.dataset.cat=n,ie.set(n,o),tt(r,s,Ne,`${he(n)} \u2014 click to filter`),r.addEventListener("click",()=>{g.has(n)?g.delete(n):g.add(n),be(),ne(),E()}),W.appendChild(r)}be()}function E(){st(D,W);const e=at(s),n=C-y,o=B-w,{w:r,h:T,plotW:d,plotH:f}=Fe(),H=dt(D,s,r,T);if(!H)return;const{ctx:t}=H;if(x.length===0){t.font=`${L()}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.",r/2,T/2);return}t.font=`${L(-1)}px system-ui, sans-serif`;const _=p!==null?N(p).unit:void 0,ye=Ae(y,C,d,rt(t,Math.max(Math.abs(y),Math.abs(C)),_)),Te=Ae(w,B,f,()=>ct),oe=Q(Math.max(Math.abs(y),Math.abs(C)),p!==null?N(p).unit:void 0,ye.step||void 0),se=Q(Math.max(Math.abs(w),Math.abs(B)),b!==null?N(b).unit:void 0,Te.step||void 0);t.font=`${L(-1)}px system-ui, sans-serif`,t.strokeStyle=e.grid,t.lineWidth=.5,t.fillStyle=e.textMuted;for(const m of ye.ticks){const u=c+(m-y)/n*d;t.beginPath(),t.moveTo(u,l),t.lineTo(u,l+f),t.stroke(),t.textAlign="center",t.textBaseline="top",t.fillText(oe.tick(m),u,l+f+4)}for(const m of Te.ticks){const u=l+(1-(m-w)/o)*f;t.beginPath(),t.moveTo(c,u),t.lineTo(c+d,u),t.stroke(),t.textAlign="right",t.textBaseline="middle",t.fillText(se.tick(m),c-4,u)}oe.unitLabel&&lt(t,oe.unitLabel,c+d/2,l+f+24,e.textMuted),se.unitLabel&&(t.save(),t.font=`${L(-1)}px system-ui, sans-serif`,t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="middle",t.translate(6,l+f/2),t.rotate(-Math.PI/2),t.fillText(`(${se.unitLabel})`,0,0),t.restore());const z=g.size===0?x:x.filter(m=>g.has(q(m))),we=z.length>5e3?Math.ceil(z.length/5e3):1;t.globalAlpha=Math.max(.15,Math.min(.7,200/(z.length/we)));for(let m=0;m<z.length;m+=we){const u=z[m],G=c+(u.x-y)/n*d,j=l+(1-(u.y-w)/o)*f;t.beginPath(),t.arc(G,j,2.5,0,Math.PI*2),t.fillStyle=xe(q(u)),t.fill()}if(t.globalAlpha=1,p!==null&&b!==null){const m=N(p),u=N(b);t.strokeStyle=e.limitLine,t.fillStyle=e.limitLine,t.lineWidth=1,t.font=`${L(-1)}px system-ui, sans-serif`,t.textBaseline="top";const G=L(-1)+2,j=m.lines.filter(i=>i.value>=y&&i.value<=C).map(i=>{const a=c+(i.value-y)/n*d;return{l:i,cx:a,start:a+2,end:a+2+t.measureText(i.label).width}}),Se=Pe(j);j.forEach(({l:i,cx:a},U)=>{De(t,i,e.limitLine,a,l,a,l+f),t.textAlign="left",Be(t,i.label,a+2,l+2+Se[U]*G,e.bg)});const le=u.lines.filter(i=>i.value>=w&&i.value<=B).map(i=>{const a=l+(1-(i.value-w)/o)*f;return{l:i,cy:a,start:a+2,end:a+2+L(-1)}}),Ge=Pe(le,1),je=J(le.map(i=>t.measureText(i.l.label).width))+8;le.forEach(({l:i,cy:a},U)=>{De(t,i,e.limitLine,c,a,c+d,a),t.textAlign="left",Be(t,i.label,c+3+Ge[U]*je,a+2,e.bg)});const ve=(i,a,U)=>i.filter($=>$.value<a||$.value>U).map($=>({value:$.value,label:$.label,side:$.value<a?"lo":"hi"})),Me={left:c,right:c+d,top:l,bottom:l+f},ae=ve(m.lines,y,C),ke=l+(j.length?(J(Se)+1)*G:0),Ue=Math.max(ae.filter(i=>i.side==="lo").length,ae.filter(i=>i.side==="hi").length);Oe(t,ae,{...Me,top:ke},"horizontal",e.limitLine,i=>Q(Math.abs(i),m.unit).tick(i),e.bg),Oe(t,ve(u.lines,w,B),{...Me,top:ke+Ue*G},"vertical",e.limitLine,i=>Q(Math.abs(i),u.unit).tick(i),e.bg),t.setLineDash([])}t.strokeStyle=e.border,t.lineWidth=1,t.beginPath(),t.moveTo(c,l),t.lineTo(c,l+f),t.moveTo(c,l+f),t.lineTo(c+d,l+f),t.stroke()}function F(){if(pe(),ue(),V.length<2||p===null||b===null){x=[],ge([]),ne(),E();return}x=v?Ke(K,p,b):Ye(fe(),p,b);let e;if(v){const n=new Set(x.map(q));e=ce.filter(o=>n.has(o))}else e=binCategories(x);if(ge(e),ne(),x.length>0){const n=x.map(_=>_.x),o=x.map(_=>_.y),r=Le(n),T=J(n),d=Le(o),f=J(o),H=(T-r)*.05||1,t=(f-d)*.05||1;y=r-H,C=T+H,w=d-t,B=f+t}E()}const He=Ze(s,()=>E());F();function _e(e,n){p=e,b=n,Re.value=String(e),We.value=String(n),F()}function ze(e){if((e.limits??"both")===(M.limits??"both")){M=e;return}M=e,ue(),E()}return{card:s,setXY:_e,setAxisPrefs:ze,destroy:()=>He.disconnect()}}
1
+ import{NO_DATA_FILL as Ne}from"../../renderer/colorMap.js";import{escHtml as Z,maxOf as se,minOf as Re}from"../../core/utils.js";import{fmt as He}from"../../renderer/fmt.js";import{categorical as st}from"./palette.js";import{buildScatterData as lt,buildScatterDataGrouped as at}from"../../stats/scatter.js";import{pearsonOfPairs as rt}from"../../stats/correlation.js";import{buildFacetTable as ct}from"../../stats/facets.js";import{SPACE as D,RADIUS as dt,fontPx as z,FONT as ge,CLR as ee}from"../toolbar.js";import{fitTicks as Fe}from"../../renderer/axisTicks.js";import{wirePointInteractions as ft}from"./pointInteractions.js";import{cardShell as mt,observeResize as ut,makeTooltip as pt,attachChartTip as xt,makeTestSelect as ht,makeWaferSelect as bt,chartFillHeight as gt,applyCanvasFlow as yt,drawAxisUnit as wt,resolveChartCanvasColors as St,makeAxisFormat as le,horizontalTickSpacing as Tt,VERTICAL_TICK_SPACING_PX as kt,makeSeriesLegendItem as Mt,prepareCanvas as vt,drawOffAxisLimits as _e,limitLines as Lt,hasBothLimitKinds as Ct,makeLimitsSelect as Ot,stackLabelRows as je,strokeLimitLine as Ge,fillTextOnHalo as Xe}from"./chartShell.js";const u=52,At=16,c=16,Pt=44;export const NO_BIN_CATEGORY="none";export function binCategoryOf(x){return x===void 0?NO_BIN_CATEGORY:String(x)}export function binCategories(x){const te=new Set;let k=!1;for(const N of x)N.hbin===void 0?k=!0:te.add(N.hbin);const L=Array.from(te).sort((N,ae)=>N-ae).map(String);return k&&L.push(NO_BIN_CATEGORY),L}export function renderScatterPanel(x){const{title:te="Test scatter",items:k,testDefs:L,binColors:N,onSaveImage:ae,groups:_}=x,{card:l,body:R,controlsRow:re}=mt(te,ae,x.ownerDocument),ne=L.filter(e=>e.testNumber!==void 0),C=!!(_&&_.length>0),ye=(_??[]).map(e=>e.key),Ye=new Map(ye.map((e,n)=>[e,n]));let f=x.xTestNumber??ne[0]?.testNumber??null,w=x.yTestNumber??ne[1]?.testNumber??f,j=null;const we=()=>j!==null?k[j]?[k[j]]:[]:k;function Se(e,n,o){const a=l.ownerDocument.createElement("label");Object.assign(a.style,{display:"inline-flex",alignItems:"center",gap:D.xs,fontSize:ge.body,color:ee.label});const g=l.ownerDocument.createElement("span");g.textContent=e;const r=ht(ne,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:l.ownerDocument});return a.append(g,r),{wrap:a,select:r}}const{wrap:Ue,select:Ke}=Se("X:",f,e=>{f=e,K()}),{wrap:Ve,select:qe}=Se("Y:",w,e=>{w=e,K()});re.append(Ue,Ve);let O=x.axisPrefs??{clipOutliers:!1};const Te=l.ownerDocument.createElement("span");re.appendChild(Te);function ke(){const e=[f,w].map(n=>L.find(o=>o.testNumber===n));Te.replaceChildren(...e.some(Ct)?[Ot(O.limits??"both",n=>{O={...O,limits:n},x.onAxisPrefsChange?.(O),v()},l.ownerDocument)]:[])}!C&&k.length>1&&re.appendChild(bt(k,j,e=>{j=e,K()},{ownerDocument:l.ownerDocument}));const H=l.ownerDocument.createElement("div");H.dataset.wmapCaption="1",Object.assign(H.style,{color:ee.warnText,background:ee.warnBg,border:`1px solid ${ee.warnBorder}`,borderRadius:dt.control,padding:`${D.xs} ${D.md}`,fontSize:ge.body,marginBottom:D.xs,display:"none"}),l.insertBefore(H,R);function Me(){const e=C?[]:ct(we(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);e.length>0?(H.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.`,H.style.display=""):H.style.display="none"}Me();const oe=l.ownerDocument.createElement("div");oe.dataset.wmapCaption="1",Object.assign(oe.style,{color:ee.label,fontSize:ge.body,marginBottom:D.xs}),l.insertBefore(oe,R);function G(){const e=(C?"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")+(fe?` \xB7 click a point to ${Ce}`:"")+(de?M.size>0?` \xB7 ${M.size.toLocaleString()} selected (click empty space to clear)`:" \xB7 drag to select dies":""),n=h.size===0?b:b.filter(g=>h.has(B(g))),{r:o,n:a}=rt(n);oe.textContent=o===null?`${e} \xB7 n = ${a.toLocaleString()}`:`${e} \xB7 r = ${o.toFixed(3)} \xB7 n = ${a.toLocaleString()}`}const B=e=>C?e.group??"\u2014":binCategoryOf(e.hbin),ve=e=>C?st(Ye.get(e)??0):e===NO_BIN_CATEGORY?Ne:N?.get(Number(e))??Ne,ce=e=>C?e:e===NO_BIN_CATEGORY?"No bin data":`Bin ${e}`,h=new Set,X=l.ownerDocument.createElement("div");Object.assign(X.style,{display:"flex",flexWrap:"wrap",gap:D.sm,marginTop:D.md,marginBottom:D.xl}),R.appendChild(X);const A=l.ownerDocument.createElement("canvas");A.style.display="block",A.style.cursor="crosshair",R.appendChild(A);const Le=pt(l);let b=[],y=0,P=1,S=0,E=1,Y=[],ie=null;const M=new Set,de=x.onSelect,fe=x.onOpen,Ce=x.openActionLabel??"open this wafer",Oe=new Map;for(const e of[...k,...(_??[]).flatMap(n=>n.items)])e.waferIndex!==void 0&&e.label&&Oe.set(e.waferIndex,e.label);function U(e){const n=L.find(o=>o.testNumber===e);return{unit:n?.unit,lines:Lt(n,O.limits)}}function Ae(){const e=R.clientWidth,n=Math.max(200,Math.min(400,e*.65)),o=gt(l,R,A,n);return{w:e,h:o,plotW:Math.max(10,e-u-At),plotH:Math.max(10,o-c-Pt)}}const me=new Map;function Pe(){for(const[e,n]of me){const o=h.has(e);n.setState({selected:o,dimmed:h.size>0&&!o})}}function $e(e){X.innerHTML="",me.clear(),h.clear();for(const n of e){const o=Mt(ce(n),ve(n),l.ownerDocument),a=o.el;a.dataset.cat=n,me.set(n,o),xt(a,l,Le,`${ce(n)} \u2014 click to filter`),a.addEventListener("click",()=>{h.has(n)?h.delete(n):h.add(n),Pe(),G(),v()}),X.appendChild(a)}Pe()}function v(){yt(A,X);const e=St(l),n=P-y,o=E-S,{w:a,h:g,plotW:r,plotH:s}=Ae(),W=vt(A,l,a,g);if(!W)return;const{ctx:t}=W;if(b.length===0){Y=[],t.font=`${z()}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.",a/2,g/2);return}t.font=`${z(-1)}px system-ui, sans-serif`;const T=f!==null?U(f).unit:void 0,V=Fe(y,P,r,Tt(t,Math.max(Math.abs(y),Math.abs(P)),T)),q=Fe(S,E,s,()=>kt),ue=le(Math.max(Math.abs(y),Math.abs(P)),f!==null?U(f).unit:void 0,V.step||void 0),pe=le(Math.max(Math.abs(S),Math.abs(E)),w!==null?U(w).unit:void 0,q.step||void 0);t.font=`${z(-1)}px system-ui, sans-serif`,t.strokeStyle=e.grid,t.lineWidth=.5,t.fillStyle=e.textMuted;for(const m of V.ticks){const d=u+(m-y)/n*r;t.beginPath(),t.moveTo(d,c),t.lineTo(d,c+s),t.stroke(),t.textAlign="center",t.textBaseline="top",t.fillText(ue.tick(m),d,c+s+4)}for(const m of q.ticks){const d=c+(1-(m-S)/o)*s;t.beginPath(),t.moveTo(u,d),t.lineTo(u+r,d),t.stroke(),t.textAlign="right",t.textBaseline="middle",t.fillText(pe.tick(m),u-4,d)}ue.unitLabel&&wt(t,ue.unitLabel,u+r/2,c+s+24,e.textMuted),pe.unitLabel&&(t.save(),t.font=`${z(-1)}px system-ui, sans-serif`,t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="middle",t.translate(6,c+s/2),t.rotate(-Math.PI/2),t.fillText(`(${pe.unitLabel})`,0,0),t.restore());const J=h.size===0?b:b.filter(m=>h.has(B(m))),De=J.length>5e3?Math.ceil(J.length/5e3):1;t.globalAlpha=Math.max(.15,Math.min(.7,200/(J.length/De))),Y=[];for(let m=0;m<J.length;m+=De){const d=J[m],$=u+(d.x-y)/n*r,I=c+(1-(d.y-S)/o)*s;Y.push({p:d,cx:$,cy:I}),t.beginPath(),t.arc($,I,2.5,0,Math.PI*2),t.fillStyle=ve(B(d)),t.fill()}t.globalAlpha=1;const xe=ie?Y.find(m=>m.p===ie):void 0;if(xe&&(t.beginPath(),t.arc(xe.cx,xe.cy,5,0,Math.PI*2),t.lineWidth=2,t.strokeStyle=e.text,t.stroke()),M.size>0){t.lineWidth=1.5,t.strokeStyle=e.text;let m=0;for(const d of M){if(h.size>0&&!h.has(B(d)))continue;if(++m>5e3)break;const $=u+(d.x-y)/n*r,I=c+(1-(d.y-S)/o)*s;t.beginPath(),t.arc($,I,4,0,Math.PI*2),t.stroke()}}if(f!==null&&w!==null){const m=U(f),d=U(w);t.strokeStyle=e.limitLine,t.fillStyle=e.limitLine,t.lineWidth=1,t.font=`${z(-1)}px system-ui, sans-serif`,t.textBaseline="top";const $=z(-1)+2,I=m.lines.filter(i=>i.value>=y&&i.value<=P).map(i=>{const p=u+(i.value-y)/n*r;return{l:i,cx:p,start:p+2,end:p+2+t.measureText(i.label).width}}),Be=je(I);I.forEach(({l:i,cx:p},Q)=>{Ge(t,i,e.limitLine,p,c,p,c+s),t.textAlign="left",Xe(t,i.label,p+2,c+2+Be[Q]*$,e.bg)});const he=d.lines.filter(i=>i.value>=S&&i.value<=E).map(i=>{const p=c+(1-(i.value-S)/o)*s;return{l:i,cy:p,start:p+2,end:p+2+z(-1)}}),nt=je(he,1),ot=se(he.map(i=>t.measureText(i.l.label).width))+8;he.forEach(({l:i,cy:p},Q)=>{Ge(t,i,e.limitLine,u,p,u+r,p),t.textAlign="left",Xe(t,i.label,u+3+nt[Q]*ot,p+2,e.bg)});const Ee=(i,p,Q)=>i.filter(F=>F.value<p||F.value>Q).map(F=>({value:F.value,label:F.label,side:F.value<p?"lo":"hi"})),We={left:u,right:u+r,top:c,bottom:c+s},be=Ee(m.lines,y,P),ze=c+(I.length?(se(Be)+1)*$:0),it=Math.max(be.filter(i=>i.side==="lo").length,be.filter(i=>i.side==="hi").length);_e(t,be,{...We,top:ze},"horizontal",e.limitLine,i=>le(Math.abs(i),m.unit).tick(i),e.bg),_e(t,Ee(d.lines,S,E),{...We,top:ze+it*$},"vertical",e.limitLine,i=>le(Math.abs(i),d.unit).tick(i),e.bg),t.setLineDash([])}t.strokeStyle=e.border,t.lineWidth=1,t.beginPath(),t.moveTo(u,c),t.lineTo(u,c+s),t.moveTo(u,c+s),t.lineTo(u+r,c+s),t.stroke()}function K(){if(ie=null,M.clear(),Me(),ke(),ne.length<2||f===null||w===null){b=[],$e([]),G(),v();return}b=C?at(_,f,w):lt(we(),f,w);let e;if(C){const n=new Set(b.map(B));e=ye.filter(o=>n.has(o))}else e=binCategories(b);if($e(e),G(),b.length>0){const n=b.map(T=>T.x),o=b.map(T=>T.y),a=Re(n),g=se(n),r=Re(o),s=se(o),W=(g-a)*.05||1,t=(s-r)*.05||1;y=a-W,P=g+W,S=r-t,E=s+t}v()}const Ie=e=>!!fe&&e.waferIndex!==void 0;function Je(e){const n=L.find(s=>s.testNumber===f),o=L.find(s=>s.testNumber===w),a=e.waferIndex!==void 0?Oe.get(e.waferIndex):void 0,g=e.die&&e.die.x!==void 0?`die (${e.die.x}, ${e.die.y})`:"",r=[a,g].filter(Boolean).join(" \xB7 ");return(r?`<strong>${Z(r)}</strong><br>`:"")+Z(`${n?.name??"X"}: ${He(e.x,n?.unit)}`)+"<br>"+Z(`${o?.name??"Y"}: ${He(e.y,o?.unit)}`)+"<br>"+Z(ce(B(e)))+(Ie(e)?`<br><em>click to ${Z(Ce)}</em>`:"")}const Qe=ft({card:l,canvas:A,tooltip:Le,drawn:()=>Y,describe:Je,clickable:Ie,onOpen:e=>{f!==null&&fe(e.waferIndex,f)},onHover:e=>{ie=e,v()},select:de?{pick:({x0:e,y0:n,x1:o,y1:a})=>{const{plotW:g,plotH:r}=Ae(),s=P-y,W=E-S;return(h.size===0?b:b.filter(T=>h.has(B(T)))).filter(T=>{const V=u+(T.x-y)/s*g,q=c+(1-(T.y-S)/W)*r;return V>=e&&V<=o&&q>=n&&q<=a})},onPicked:(e,n)=>{M.clear();for(const o of e)M.add(o);G(),v(),e.length>0&&f!==null&&de(e,n,A,f)},onClear:()=>{M.clear(),G(),v()},hasSelection:()=>M.size>0}:void 0}),Ze=ut(l,()=>v());K();function et(e,n){f=e,w=n,Ke.value=String(e),qe.value=String(n),K()}function tt(e){if((e.limits??"both")===(O.limits??"both")){O=e;return}O=e,ke(),v()}return{card:l,setXY:et,setAxisPrefs:tt,destroy:()=>{Ze.disconnect(),Qe.destroy()}}}
@@ -19,6 +19,14 @@ export interface SweepPanelOptions {
19
19
  population?: string;
20
20
  testDefs: TestDef[] | undefined;
21
21
  onSaveImage?: SaveImageHandler;
22
+ /**
23
+ * Click a level of the curve: the tests measured there (one per series that has a value at it) and its label, with the
24
+ * click, so the host can pick out the dies that were measured and open a menu on them.
25
+ */
26
+ onSelectPoint?: (sel: {
27
+ testNumbers: number[];
28
+ label: string;
29
+ }, e: MouseEvent) => void;
22
30
  ownerDocument?: Document;
23
31
  }
24
32
  export interface SweepPanelHandle {
@@ -26,6 +34,10 @@ export interface SweepPanelHandle {
26
34
  destroy: () => void;
27
35
  /** Re-aggregate over a new population, keeping the same curve definition. */
28
36
  setDies: (dies: Die[]) => void;
37
+ /** Draw a changed definition (the editor), keeping the population. */
38
+ setSpec: (spec: SweepSpec) => void;
39
+ /** The row under the title, for the host's own buttons (Edit, Duplicate, Delete). */
40
+ actions: HTMLElement;
29
41
  }
30
42
  export declare function renderSweepPanel(options: SweepPanelOptions): SweepPanelHandle;
31
43
  //# sourceMappingURL=sweep.d.ts.map
@@ -1 +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
+ import{buildSweepData as de,sweepAppliesTo as ue}from"../../stats/sweep.js";import{fmt as N}from"../../renderer/fmt.js";import{SPACE as oe,fontPx as he,FONT as ie,CLR as Y,ALPHA as me}from"../toolbar.js";import{categorical as xe}from"./palette.js";import{cardShell as ge,observeResize as be,makeTooltip as ye,positionChartTooltip as $e,renderEmptyState as re,chartFillHeight as Te,populationPhrase as ve,PADDING as J,prepareCanvas as Se,makeAxisFormat as Me,VERTICAL_TICK_SPACING_PX as Le}from"./chartShell.js";import{fitTicks as we}from"../../renderer/axisTicks.js";import{escHtml as m}from"../../core/utils.js";import{unmarkedLabel as Pe,derivedTestNote as ke,DERIVED_MARK as Q,DERIVED_KEY as Ae}from"../../renderer/testLabel.js";function O(c,x){return x?N(c,x).replace(/(\.\d*?)0+(?=\D|$)/,"$1").replace(/\.(?=\D|$)/,""):Number.isInteger(c)?String(c):String(Number(c.toPrecision(3)))}function Ce(c){const x=Pe(c.label),E=x.length>9?`${x.slice(0,8)}\u2026`:x;return c.derived?`${Q} ${E}`:E}const le=260,Ee=62,Ie=56,Ne=3;function De(c,x){const E=c.series[0]?.points[0]?.testNumber;return E===void 0?void 0:x?.find(j=>j.testNumber===E)?.unit}export function renderSweepPanel(c){const{testDefs:x,onSaveImage:E,population:j}=c;let S=c.spec,z=c.dies;const{card:g,body:L,heading:ae,controlsRow:ce}=ge(S.title,E,c.ownerDocument);g.style.alignSelf="start";const V=g.ownerDocument,D=V.createElement("div");D.dataset.wmapCaption="1",Object.assign(D.style,{color:Y.label,fontSize:ie.body,marginBottom:oe.sm}),g.insertBefore(D,L);const W=V.createElement("div");Object.assign(W.style,{color:Y.label,fontSize:ie.body,marginTop:oe.sm}),g.appendChild(W);const w=ye(g);let F=null,M=null,$;const R=n=>S.series[n]?.color??xe(n);function K(){if(L.innerHTML="",W.innerHTML="",F?.disconnect(),F=null,S.series.length===0){D.textContent="",re(L,"This sweep defines no series.");return}if(M=de(z,x,S),$=De(M,x),M.series.filter(s=>s.points.some(d=>d.count>0)).length===0){D.textContent="",re(L,ue(S,x)?"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}D.textContent=`Line = median across ${ve(M.dieCount,z.length,j)}, band = p10\u2013p90`+(S.series.length>2?" \xB7 crossing and separation measured between the first two series":"")+(c.onSelectPoint?" \xB7 click a level to chart or tabulate the dies measured there":"");const l=V.createElement("canvas");l.style.display="block",L.appendChild(l);let i=null;const P=()=>{const s=M;if(s===null)return;const d=Math.max(1,L.clientWidth),b=Math.max(le,Te(g,L,l,le)),y=Se(l,g,d,b);if(!y)return;const{ctx:e,theme:f}=y,T=t=>s.xScale==="log"?Math.log10(t):t;let a=1/0,o=-1/0,u=1/0,h=-1/0;for(const t of s.series)for(const r of t.points)r.count!==0&&(a=Math.min(a,r.p10),o=Math.max(o,r.p90),u=Math.min(u,T(r.x)),h=Math.max(h,T(r.x)));for(const t of s.separations)a=Math.min(a,t.y),o=Math.max(o,t.y);if(!Number.isFinite(a)||!Number.isFinite(o))return;a===o&&(a-=1,o+=1);const ee=(o-a)*.08;if(a-=ee,o+=ee,u===h&&(u-=.5,h+=.5),s.xScale==="log"){const t=(h-u)*.03;u-=t,h+=t}e.font=`${he(-1)}px system-ui, sans-serif`;const v=J+Ee,H=d-J-8,X=J,k=b-Ie,A=t=>k-(t-a)/(o-a)*(k-X),B=t=>v+(T(t)-u)/(h-u)*(H-v);i={left:v,right:H,xLo:u,xHi:h,yOf:A,ax:T},e.strokeStyle=f.border,e.lineWidth=1,e.beginPath(),e.moveTo(v,X),e.lineTo(v,k),e.lineTo(H,k),e.stroke(),e.fillStyle=f.text,e.textAlign="right",e.textBaseline="middle";const te=we(a,o,k-X,()=>Le),fe=Me(Math.max(Math.abs(a),Math.abs(o)),$,te.step||void 0);for(const t of te.ticks)e.fillText(fe.tickWithUnit(t),v-6,A(t));for(const t of s.separations){const r=A(t.y);e.save(),e.strokeStyle=f.limitLine,e.globalAlpha=.55,e.setLineDash([4,3]),e.beginPath(),e.moveTo(v,r),e.lineTo(H,r),e.stroke(),e.restore()}if(s.series.forEach((t,r)=>{const C=t.points.filter(p=>p.count>0);if(!(C.length<2)){e.save(),e.fillStyle=R(r),e.globalAlpha=.14,e.beginPath(),C.forEach((p,I)=>{const _=B(p.x),se=A(p.p90);I===0?e.moveTo(_,se):e.lineTo(_,se)});for(let p=C.length-1;p>=0;p--){const I=C[p];e.lineTo(B(I.x),A(I.p10))}e.closePath(),e.fill(),e.restore()}}),s.series.forEach((t,r)=>{e.strokeStyle=R(r),e.lineWidth=1.75,e.beginPath();let C=!1;for(const p of t.points){if(p.count===0){C=!1;continue}const I=B(p.x),_=A(p.median);C?e.lineTo(I,_):e.moveTo(I,_),C=!0}e.stroke(),e.fillStyle=R(r);for(const p of t.points)p.count!==0&&(e.beginPath(),e.arc(B(p.x),A(p.median),Ne,0,Math.PI*2),e.fill())}),s.crossing){const t=B(s.crossing.x),r=A(s.crossing.y);e.save(),e.strokeStyle=f.text,e.globalAlpha=.45,e.setLineDash([3,3]),e.beginPath(),e.moveTo(t,r),e.lineTo(t,k),e.stroke(),e.restore(),e.save(),e.strokeStyle=f.text,e.lineWidth=2,e.beginPath(),e.arc(t,r,5.5,0,Math.PI*2),e.stroke(),e.restore()}e.fillStyle=f.text,e.textBaseline="top",e.textAlign="center";const G=s.xIsPhysical?[...new Set(s.series.flatMap(t=>t.points.map(r=>r.x)))].sort((t,r)=>t-r).map(t=>({x:t,label:O(t,s.xUnit)})):(s.series[0]?.points??[]).map(t=>({x:t.x,label:Ce(t)})),pe=Math.max(1,Math.ceil(G.length*46/Math.max(1,H-v)));G.forEach((t,r)=>{r%pe!==0&&r!==G.length-1||e.fillText(t.label,B(t.x),k+6)}),e.save(),e.fillStyle=f.text,e.globalAlpha=.75,e.textAlign="center",e.textBaseline="bottom",e.fillText(s.xLabel,(v+H)/2,b-4),e.restore(),e.save(),e.fillStyle=f.text,e.globalAlpha=.75,e.translate(12,(X+k)/2),e.rotate(-Math.PI/2),e.textBaseline="top",e.fillText(s.yLabel+($?` (${$})`:""),0,0),e.restore(),e.textAlign="left",e.textBaseline="middle";let U=v+4;const ne=X+8;s.series.forEach((t,r)=>{e.fillStyle=R(r),e.fillRect(U,ne-4,10,3),U+=14,e.fillStyle=f.text,e.fillText(t.label,U,ne),U+=e.measureText(t.label).width+16})};P(),F=be(L,P);const q=s=>{const d=M;if(d===null||i===null)return null;const b=s.clientX-l.getBoundingClientRect().left,y=i.xLo+(b-i.left)/Math.max(1,i.right-i.left)*(i.xHi-i.xLo);let e=null;for(const f of d.series)for(const T of f.points){if(T.count===0)continue;const a=Math.abs(i.ax(T.x)-y);(e===null||a<e.dist)&&(e={x:T.x,dist:a})}return e===null?null:e.x};l.addEventListener("mousemove",s=>{const d=M,b=q(s);if(d===null||b===null){w.style.display="none";return}const y={x:b},e=d.series.map(o=>o.points.find(u=>u.x===y.x&&u.count>0)),f=d.series.map((o,u)=>{const h=e[u];return h===void 0?"":`<span style="color:${R(u)}">\u25A0</span> ${h.derived?`${Q} `:""}${m(o.label)}: <strong>${m(N(h.median,$))}</strong><br><span style="opacity:${me.muted}">p10 ${m(N(h.p10,$))} \u2013 p90 ${m(N(h.p90,$))} \xB7 n=${h.count.toLocaleString()}</span>`}).filter(Boolean);if(f.length===0){w.style.display="none";return}const T=d.series.flatMap(o=>o.points).find(o=>o.x===y.x&&o.count>0),a=[...new Set(e.map(o=>ke(o)).filter(o=>o!==void 0))];w.innerHTML=`<strong>${m(T?.label??"")}</strong><br>`+f.join("<br>")+a.map(o=>`<br><em>${m(o)}</em>`).join(""),w.style.display="block",$e(w,g,s.clientX,s.clientY)}),l.addEventListener("mouseleave",()=>{w.style.display="none"}),c.onSelectPoint&&(l.style.cursor="pointer",l.addEventListener("click",s=>{const d=M,b=q(s);if(d===null||b===null)return;const y=d.series.flatMap(e=>e.points.filter(f=>f.x===b&&f.count>0));y.length!==0&&(w.style.display="none",c.onSelectPoint({testNumbers:y.map(e=>e.testNumber),label:y[0].label},s))})),Z()}function Z(){const n=M;if(n===null)return;const l=[];n.notMeasured!==void 0?l.push(`<strong>Crossing and widths:</strong> not measured \u2014 ${m(n.notMeasured)}`):n.crossing?l.push(`<strong>Crossing:</strong> ${m(`${n.xLabel} = ${O(n.crossing.x,n.xUnit)}`)} \xB7 ${m(`${n.yLabel} ${N(n.crossing.y,$)}`)}`+(n.crossing.multiple?" <em>(first of several \u2014 the curves cross more than once)</em>":"")):(S.crossing??!0)&&S.series.length>=2&&l.push("<strong>Crossing:</strong> the first two series do not cross over the swept range");for(const i of n.separations)if(i.distance!==null){const P=i.ratio!==void 0&&i.from!==void 0&&i.to!==void 0?`\xD7${Number(i.ratio.toPrecision(3))} on the ${n.xLabel} axis (${O(i.from,n.xUnit)} \u2192 ${O(i.to,n.xUnit)})`:`${O(i.distance,n.xUnit)} on the ${n.xLabel} axis`;l.push(`<strong>Width at ${m(N(i.y,$))}:</strong> ${m(P)}`)}else{const P=i.reason==="second-series-never-reaches"?n.series[1]?.label:n.series[0]?.label;l.push(`<strong>Width at ${m(N(i.y,$))}:</strong> not measurable \u2014 ${m(P??"a series")} never reaches this level`)}n.series.some(i=>i.points.some(P=>P.derived))&&l.push(`${Q} ${m(Ae)} \u2014 hover a point for the expression`);for(const i of n.warnings)l.push(`<span style="color:${Y.errText}">${m(i)}</span>`);W.innerHTML=l.join("<br>")}return K(),{card:g,destroy:()=>{F?.disconnect(),w.remove(),g.remove()},setDies:n=>{z=n,K()},setSpec:n=>{S=n,ae.textContent=n.title,g.dataset.wmapChartTitle=n.title,K()},actions:ce}}
@@ -1,3 +1,4 @@
1
+ import { type TestPassKind } from '../../stats/testPassRate.js';
1
2
  import type { TestPassRateItem } from '../../stats/testPassRate.js';
2
3
  import type { TestDef } from '../../renderer/buildWaferMap.js';
3
4
  import { type SaveImageHandler } from './chartShell.js';
@@ -10,6 +11,12 @@ export interface TestPassRatePanelOptions {
10
11
  }[];
11
12
  testDefs: TestDef[] | undefined;
12
13
  onSaveImage?: SaveImageHandler;
14
+ /** Click a test's row: the host picks out the dies that fail it (judged the way the card is showing). */
15
+ onSelectTest?: (row: {
16
+ testNumber: number;
17
+ label: string;
18
+ kind: TestPassKind;
19
+ }, e: MouseEvent) => void;
13
20
  ownerDocument?: Document;
14
21
  }
15
22
  export interface TestPassRatePanelHandle {
@@ -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 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
+ import{buildTestPassRateData as F,hasJudgeableTests as N}from"../../stats/testPassRate.js";import{categorical as B,QUANTITY as I}from"./palette.js";import{SPACE as _,FONT as H,CLR as L}from"../toolbar.js";import{cardShell as k,makeTooltip as A,makeSegmented as O,renderEmptyState as y}from"./chartShell.js";import{renderGroupedBarPlot as j}from"./groupedBarPlot.js";import{escHtml as f}from"../../core/utils.js";const M=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(i){const{groups:b,testDefs:h,onSaveImage:$}=i,p=i.ownerDocument??document,C=b.flatMap(t=>t.items.flatMap(o=>o.dies??[])),m=["spec","testFlag","functional"].filter(t=>N(h,t,C));let s=m[0]??"spec";const T=t=>U[t],{card:n,heading:P,body:c,controlsRow:R}=k(T(s),$,p);n.style.alignSelf="start",m.length>1&&R.appendChild(O(m.map(t=>[t,K[t]]),s,t=>{s=t,P.textContent=T(s),S()},p));const r=p.createElement("div");r.dataset.wmapCaption="1",Object.assign(r.style,{color:L.label,fontSize:H.body,marginBottom:_.sm}),n.insertBefore(r,c);const D=A(n);let g=null;function S(){if(g?.destroy(),g=null,c.innerHTML="",m.length===0){r.textContent="",y(c,"No tests with a pass/fail verdict \u2014 parametric tests need test limits, functional tests need recorded results.");return}const t=F(b,h,s);if(t.rows.length===0){r.textContent="",y(c,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 o=t.groups.length>1,v=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(r.textContent=v+(o?" \xB7 one bar per group":"")+" \xB7 worst first"+(i.onSelectTest?" \xB7 click a test to chart or tabulate the dies that fail it":""),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:L.warnText}),r.appendChild(e)}const u=t.rows,x=e=>o?B(e):I;g=j(n,c,D,{rows:u.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:M,maxVisibleRows:G,showLegend:o,maxLabelChars:14,emptyBarText:"no data",colorOf:x,onRowClick:i.onSelectTest?(e,l,a)=>i.onSelectTest({testNumber:u[e].testNumber,label:u[e].label,kind:s},a):void 0,tooltipHtml:(e,l)=>{const a=u[e],d=a.byGroup[l],w=o?` \xB7 ${f(String(t.groups[l]))}`:"";if(d.passRatePercent===null)return`<strong>${f(a.label)}</strong>${w}<br>no dies with a verdict`;const E=s==="spec"&&!o&&(a.failLowDies||a.failHighDies)?`<br>${a.failLowDies??0} below the low limit \xB7 ${a.failHighDies??0} above the high limit`:"";return`<strong>${f(a.label)}</strong>${w}<br>${d.passRatePercent.toFixed(1)}% pass<br>${d.passDies.toLocaleString()} pass \xB7 ${d.failDies.toLocaleString()} fail \xB7 n = ${d.totalDies.toLocaleString()}`+E}})}return S(),{card:n,destroy:()=>{g?.destroy(),D.remove(),n.remove()}}}
@@ -11,6 +11,17 @@ export interface TrendPanelOptions {
11
11
  onOpen?: (key: number, testNumber: number) => void;
12
12
  /** Right-click on a wafer's point — see `WaferContextMenuHandler`. */
13
13
  onWaferContextMenu?: WaferContextMenuHandler;
14
+ /**
15
+ * Drag across the trend to pick a run of wafers: their `key`s (the wafer indexes), the test shown, where the pointer
16
+ * was released and the canvas to anchor a menu on. The wafers' own dies are the population.
17
+ */
18
+ onSelectWafers?: (sel: {
19
+ keys: number[];
20
+ testNumber: number;
21
+ }, at: {
22
+ x: number;
23
+ y: number;
24
+ }, anchor: HTMLElement) => void;
14
25
  /**
15
26
  * What the dashed reference line is, as a noun phrase — "lot mean" when the
16
27
  * items are one lot, else "mean of all wafers" (see stats/population.ts).
@@ -1 +1 @@
1
- import{buildTestTrendData as ke,trendCentre as we}from"../../stats/trend.js";import{isParametricTest as Ce}from"../../renderer/buildWaferMap.js";import{fmt as W}from"../../renderer/fmt.js";import{SPACE as re,fontPx as ce,FONT as Ae,CLR as Me}from"../toolbar.js";import{QUANTITY as G}from"./palette.js";import{cardShell as Pe,observeResize as Se,makeTooltip as Ne,positionChartTooltip as Ee,makeLinkedTestSelect as Ie,makeLinkedAxisPrefs as De,renderEmptyState as U,chartFillHeight as Oe,PADDING as j,resolveAxisRange as We,shouldIncludeLimitsByDefault as He,drawOffAxisLimits as $e,limitLines as me,limitExtent as de,hasBothLimitKinds as Re,stackLabelRows as Be,strokeLimitLine as Fe,makeAxisFormat as _e,VERTICAL_TICK_SPACING_PX as Xe,WAFER_MENU_HINT as ze,prepareCanvas as Ge}from"./chartShell.js";import{fitTicks as Ue}from"../../renderer/axisTicks.js";import{escHtml as K}from"../../core/utils.js";import{maxOf as je}from"../../core/utils.js";const fe=220,Ke=56,Ye=34,Qe=3.5;export function renderTrendPanel(d){const{items:Y,testDefs:ue,onSaveImage:he,onOpen:C,centreLabel:Q="mean of all wafers"}=d,pe=d.title??"Wafer-to-wafer trend",{card:a,body:u,controlsRow:V}=Pe(pe,he,d.ownerDocument);a.style.alignSelf="start";const N=ue.filter(s=>s.testNumber!==void 0&&Ce(s));let h=d.selectedTestNumber??N[0]?.testNumber??null,H=0;const q=Ie(N,h,s=>{h=s,A(),d.onTestChange?.(s)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument});V.appendChild(q.el);const $=a.ownerDocument.createElement("span");Object.assign($.style,{display:"inline-flex",gap:re.lg,alignItems:"center"}),V.appendChild($);const R=De($,d.axisPrefs,s=>{d.onAxisPrefsChange?.(s),A()},a.ownerDocument),w=a.ownerDocument.createElement("div");Object.assign(w.style,{color:Me.label,fontSize:Ae.body,marginBottom:re.sm}),a.insertBefore(w,u);const b=Ne(a);let E=null,i=[];function xe(s){return N.find(B=>B.testNumber===s)}function A(){const{includeLimits:s,clipOutliers:B,limits:be}=R.get();if(u.innerHTML="",E?.disconnect(),E=null,N.length===0||h===null){w.textContent="",U(u,"No parametric test data available for a trend.");return}if(Y.length<2){w.textContent="",U(u,"A wafer-to-wafer trend needs at least two wafers.");return}if(i=ke(Y,h),!i.some(o=>o.count>0)){w.textContent="",U(u,"No values recorded for this test.");return}const p=xe(h),I=me(p,be),g=we(i),J=()=>{w.textContent=`Point = wafer mean, whisker = \xB11\u03C3, dashed = ${Q} \xB7 wafers in slot order`+(C?" \xB7 click a point to open that wafer":"")+(H?` \xB7 axis clipped, ${H} outside`:"")};J();const m=a.ownerDocument.createElement("canvas");m.style.display="block",u.appendChild(m);let M=null;const Z=()=>{const o=Math.max(1,u.clientWidth),r=Math.max(fe,Oe(a,u,m,fe)),n=Ge(m,a,o,r);if(!n)return;const{ctx:e,theme:y}=n;let P=1/0,S=-1/0;const ee=[];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),ee.push(t.mean-t.stddev,t.mean+t.stddev));if(!Number.isFinite(P)||!Number.isFinite(S))return;const te=s??He(P,S,de(I).lo,de(I).hi);R.sync(te,me(p).length>0,Re(p));const D=We({dataMin:P,dataMax:S,limits:I,includeLimits:te,clipOutliers:B,values:ee});H=D.clippedCount,J();let x=D.lo,v=D.hi;const ne=(v-x)*.08;x-=ne,v+=ne;const c=j+Ke;e.font=`${ce(-1)}px system-ui, sans-serif`;const ie=t=>t.length>8?`${t.slice(0,7)}\u2026`:t,ge=i.length?Math.ceil(Math.max(e.measureText(ie(i[0].label)).width,e.measureText(ie(i[i.length-1].label)).width)/2):0,T=o-j-ge,O=j,L=r-Ye,k=t=>L-(t-x)/(v-x)*(L-O),se=i.length>1?(T-c)/(i.length-1):0;M={left:c,step:se};const _=t=>i.length>1?c+t*se:(c+T)/2;e.strokeStyle=y.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,O),e.lineTo(c,L),e.lineTo(T,L),e.stroke(),e.fillStyle=y.text,e.textAlign="right",e.textBaseline="middle";const oe=Ue(x,v,L-O,()=>Xe),ye=_e(Math.max(Math.abs(x),Math.abs(v)),p?.unit,oe.step||void 0);for(const t of oe.ticks)e.fillText(ye.tickWithUnit(t),c-6,k(t));$e(e,D.offAxis,{left:c,right:T,top:O,bottom:L},"vertical",y.limitLine,t=>W(t,p?.unit));const le=ce(-1),X=I.filter(t=>t.value>=x&&t.value<=v).map(t=>{const l=k(t.value);return{l:t,y:l,start:l-6-le/2,end:l-6+le/2}}),ve=Be(X,1),Te=je(X.map(t=>e.measureText(t.l.label).width))+8;if(X.forEach(({l:t,y:l},f)=>{e.save(),e.globalAlpha=.7,Fe(e,t,y.limitLine,c,l,T,l),e.restore(),e.save(),e.fillStyle=y.limitLine,e.textAlign="left",e.fillText(t.label,c+3+ve[f]*Te,l-6),e.restore()}),g!==null&&g>=x&&g<=v){const t=k(g);e.save(),e.strokeStyle=y.text,e.globalAlpha=.5,e.setLineDash([3,3]),e.beginPath(),e.moveTo(c,t),e.lineTo(T,t),e.stroke(),e.restore()}e.strokeStyle=G,e.lineWidth=1.5,e.beginPath();let z=!1;i.forEach((t,l)=>{if(!t.count||!Number.isFinite(t.mean)){z=!1;return}const f=_(l),ae=k(t.mean);z?e.lineTo(f,ae):e.moveTo(f,ae),z=!0}),e.stroke(),i.forEach((t,l)=>{if(!t.count||!Number.isFinite(t.mean))return;const f=_(l);t.stddev>0&&(e.save(),e.strokeStyle=G,e.globalAlpha=.55,e.lineWidth=1,e.beginPath(),e.moveTo(f,k(t.mean-t.stddev)),e.lineTo(f,k(t.mean+t.stddev)),e.stroke(),e.restore()),e.fillStyle=G,e.beginPath(),e.arc(f,k(t.mean),Qe,0,Math.PI*2),e.fill()}),e.fillStyle=y.text,e.textBaseline="top",e.textAlign="center";const Le=Math.max(1,Math.ceil(i.length*42/Math.max(1,T-c)));i.forEach((t,l)=>{if(l%Le!==0&&l!==i.length-1)return;const f=t.label.length>8?`${t.label.slice(0,7)}\u2026`:t.label;e.fillText(f,_(l),L+6)})},F=o=>{if(!M||M.step<=0)return-1;const r=m.getBoundingClientRect(),n=Math.round((o.clientX-r.left-M.left)/M.step);return n>=0&&n<i.length&&i[n].count>0?n:-1};m.addEventListener("mousemove",o=>{const r=F(o);if(r<0){b.style.display="none",m.style.cursor="default";return}const n=i[r];m.style.cursor=C&&n.key!==void 0?"pointer":"default",b.innerHTML=`<strong>${K(n.label)}</strong><br>`+K(`mean ${W(n.mean,p?.unit)} \xB7 \u03C3 ${W(n.stddev,p?.unit)}`)+`<br>n = ${n.count.toLocaleString()}`+(g!==null?"<br>"+K(`\u0394 vs ${Q} ${n.mean-g>=0?"+":""}${W(n.mean-g,p?.unit)}`):"")+(C&&n.key!==void 0?"<br><em>click to open this wafer</em>":"")+(d.onWaferContextMenu&&n.key!==void 0?ze:""),b.style.display="block",Ee(b,a,o.clientX,o.clientY)}),m.addEventListener("mouseleave",()=>{b.style.display="none"}),m.addEventListener("click",o=>{const r=F(o);if(r<0||!C)return;const n=i[r];n.key!==void 0&&h!==null&&C(n.key,h)}),m.addEventListener("contextmenu",o=>{const r=F(o),n=r<0?void 0:i[r].key;n===void 0||!d.onWaferContextMenu||(b.style.display="none",d.onWaferContextMenu(n,h??void 0,o))}),Z(),E=Se(u,Z)}return A(),{card:a,destroy:()=>{E?.disconnect(),b.remove(),a.remove()},setTest:s=>{q.set(s)&&(h=s,A())},setAxisPrefs:s=>{R.set(s)&&A()}}}
1
+ import{buildTestTrendData as Se,trendCentre as Ce}from"../../stats/trend.js";import{isParametricTest as Ae}from"../../renderer/buildWaferMap.js";import{fmt as H}from"../../renderer/fmt.js";import{SPACE as ce,fontPx as me,FONT as Pe,CLR as Me}from"../toolbar.js";import{QUANTITY as X}from"./palette.js";import{wirePointInteractions as Ne}from"./pointInteractions.js";import{cardShell as Ie,observeResize as We,makeTooltip as Ee,makeLinkedTestSelect as De,makeLinkedAxisPrefs as Oe,renderEmptyState as j,chartFillHeight as He,PADDING as K,resolveAxisRange as $e,shouldIncludeLimitsByDefault as Re,drawOffAxisLimits as _e,limitLines as fe,limitExtent as de,hasBothLimitKinds as Be,stackLabelRows as Fe,strokeLimitLine as ze,makeAxisFormat as Ge,VERTICAL_TICK_SPACING_PX as Ue,WAFER_MENU_HINT as Xe,prepareCanvas as je}from"./chartShell.js";import{fitTicks as Ke}from"../../renderer/axisTicks.js";import{escHtml as Q}from"../../core/utils.js";import{maxOf as Qe}from"../../core/utils.js";const ue=220,Ve=56,Ye=34,he=3.5;export function renderTrendPanel(a){const{items:V,testDefs:pe,onSaveImage:xe,onOpen:M,centreLabel:Y="mean of all wafers"}=a,be=a.title??"Wafer-to-wafer trend",{card:c,body:h,controlsRow:q}=Ie(be,xe,a.ownerDocument);c.style.alignSelf="start";const N=pe.filter(l=>l.testNumber!==void 0&&Ae(l));let f=a.selectedTestNumber??N[0]?.testNumber??null,$=0;const J=De(N,f,l=>{f=l,S(),a.onTestChange?.(l)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:c.ownerDocument});q.appendChild(J.el);const R=c.ownerDocument.createElement("span");Object.assign(R.style,{display:"inline-flex",gap:ce.lg,alignItems:"center"}),q.appendChild(R);const _=Oe(R,a.axisPrefs,l=>{a.onAxisPrefsChange?.(l),S()},c.ownerDocument),y=c.ownerDocument.createElement("div");y.dataset.wmapCaption="1",Object.assign(y.style,{color:Me.label,fontSize:Pe.body,marginBottom:ce.sm}),c.insertBefore(y,h);const B=Ee(c);let I=null,s=[];function ge(l){return N.find(F=>F.testNumber===l)}function S(){const{includeLimits:l,clipOutliers:F,limits:ye}=_.get();if(h.innerHTML="",I?.disconnect(),I=null,N.length===0||f===null){y.textContent="",j(h,"No parametric test data available for a trend.");return}if(V.length<2){y.textContent="",j(h,"A wafer-to-wafer trend needs at least two wafers.");return}if(s=Se(V,f),!s.some(i=>i.count>0)){y.textContent="",j(h,"No values recorded for this test.");return}const p=ge(f),W=fe(p,ye),v=Ce(s),Z=()=>{y.textContent=`Point = wafer mean, whisker = \xB11\u03C3, dashed = ${Y} \xB7 wafers in slot order`+(M?" \xB7 click a point to open that wafer":"")+(a.onSelectWafers?" \xB7 drag across the plot to select wafers":"")+($?` \xB7 axis clipped, ${$} outside`:"")};Z();const x=c.ownerDocument.createElement("canvas");x.style.display="block",h.appendChild(x);let T=null;const C=new Set,E=()=>{const i=Math.max(1,h.clientWidth),n=Math.max(ue,He(c,h,x,ue)),o=je(x,c,i,n);if(!o)return;const{ctx:e,theme:u}=o;let A=1/0,P=-1/0;const te=[];for(const t of s)!t.count||!Number.isFinite(t.mean)||(A=Math.min(A,t.mean-t.stddev),P=Math.max(P,t.mean+t.stddev),te.push(t.mean-t.stddev,t.mean+t.stddev));if(!Number.isFinite(A)||!Number.isFinite(P))return;const ne=l??Re(A,P,de(W).lo,de(W).hi);_.sync(ne,fe(p).length>0,Be(p));const D=$e({dataMin:A,dataMax:P,limits:W,includeLimits:ne,clipOutliers:F,values:te});$=D.clippedCount,Z();let b=D.lo,k=D.hi;const se=(k-b)*.08;b-=se,k+=se;const m=K+Ve;e.font=`${me(-1)}px system-ui, sans-serif`;const ie=t=>t.length>8?`${t.slice(0,7)}\u2026`:t,ve=s.length?Math.ceil(Math.max(e.measureText(ie(s[0].label)).width,e.measureText(ie(s[s.length-1].label)).width)/2):0,w=i-K-ve,O=K,L=n-Ye,g=t=>L-(t-b)/(k-b)*(L-O),oe=s.length>1?(w-m)/(s.length-1):0;T={left:m,step:oe};const z=t=>s.length>1?m+t*oe:(m+w)/2;e.strokeStyle=u.border,e.lineWidth=1,e.beginPath(),e.moveTo(m,O),e.lineTo(m,L),e.lineTo(w,L),e.stroke(),e.fillStyle=u.text,e.textAlign="right",e.textBaseline="middle";const le=Ke(b,k,L-O,()=>Ue),Te=Ge(Math.max(Math.abs(b),Math.abs(k)),p?.unit,le.step||void 0);for(const t of le.ticks)e.fillText(Te.tickWithUnit(t),m-6,g(t));_e(e,D.offAxis,{left:m,right:w,top:O,bottom:L},"vertical",u.limitLine,t=>H(t,p?.unit));const re=me(-1),G=W.filter(t=>t.value>=b&&t.value<=k).map(t=>{const r=g(t.value);return{l:t,y:r,start:r-6-re/2,end:r-6+re/2}}),ke=Fe(G,1),we=Qe(G.map(t=>e.measureText(t.l.label).width))+8;if(G.forEach(({l:t,y:r},d)=>{e.save(),e.globalAlpha=.7,ze(e,t,u.limitLine,m,r,w,r),e.restore(),e.save(),e.fillStyle=u.limitLine,e.textAlign="left",e.fillText(t.label,m+3+ke[d]*we,r-6),e.restore()}),v!==null&&v>=b&&v<=k){const t=g(v);e.save(),e.strokeStyle=u.text,e.globalAlpha=.5,e.setLineDash([3,3]),e.beginPath(),e.moveTo(m,t),e.lineTo(w,t),e.stroke(),e.restore()}e.strokeStyle=X,e.lineWidth=1.5,e.beginPath();let U=!1;s.forEach((t,r)=>{if(!t.count||!Number.isFinite(t.mean)){U=!1;return}const d=z(r),ae=g(t.mean);U?e.lineTo(d,ae):e.moveTo(d,ae),U=!0}),e.stroke(),s.forEach((t,r)=>{if(!t.count||!Number.isFinite(t.mean))return;const d=z(r);t.stddev>0&&(e.save(),e.strokeStyle=X,e.globalAlpha=.55,e.lineWidth=1,e.beginPath(),e.moveTo(d,g(t.mean-t.stddev)),e.lineTo(d,g(t.mean+t.stddev)),e.stroke(),e.restore()),e.fillStyle=X,e.beginPath(),e.arc(d,g(t.mean),he,0,Math.PI*2),e.fill(),C.has(r)&&(e.beginPath(),e.arc(d,g(t.mean),he+3,0,Math.PI*2),e.strokeStyle=u.text,e.lineWidth=1.5,e.stroke())}),e.fillStyle=u.text,e.textBaseline="top",e.textAlign="center";const Le=Math.max(1,Math.ceil(s.length*42/Math.max(1,w-m)));s.forEach((t,r)=>{if(r%Le!==0&&r!==s.length-1)return;const d=t.label.length>8?`${t.label.slice(0,7)}\u2026`:t.label;e.fillText(d,z(r),L+6)})},ee=i=>{if(!T||T.step<=0)return-1;const n=x.getBoundingClientRect(),o=Math.round((i.clientX-n.left-T.left)/T.step);return o>=0&&o<s.length&&s[o].count>0?o:-1};Ne({card:c,canvas:x,tooltip:B,drawn:()=>[],locate:i=>{const n=ee(i);return n<0?null:n},describe:i=>{const n=s[i];return`<strong>${Q(n.label)}</strong><br>`+Q(`mean ${H(n.mean,p?.unit)} \xB7 \u03C3 ${H(n.stddev,p?.unit)}`)+`<br>n = ${n.count.toLocaleString()}`+(v!==null?"<br>"+Q(`\u0394 vs ${Y} ${n.mean-v>=0?"+":""}${H(n.mean-v,p?.unit)}`):"")+(M&&n.key!==void 0?"<br><em>click to open this wafer</em>":"")+(a.onSelectWafers?"<br><em>drag across the plot to select wafers</em>":"")+(a.onWaferContextMenu&&n.key!==void 0?Xe:"")},clickable:i=>!!M&&s[i].key!==void 0,onOpen:i=>{const n=s[i];n.key!==void 0&&f!==null&&M(n.key,f)},onHover:()=>{},select:a.onSelectWafers?{pick:({x0:i,x1:n})=>{if(!T)return[];const o=T;return s.map((e,u)=>u).filter(e=>s[e].count>0&&o.left+e*o.step>=i&&o.left+e*o.step<=n)},onPicked:(i,n)=>{C.clear();for(const e of i)C.add(e);E();const o=i.map(e=>s[e].key).filter(e=>e!==void 0);o.length>0&&f!==null&&a.onSelectWafers({keys:o,testNumber:f},n,x)},onClear:()=>{C.clear(),E()},hasSelection:()=>C.size>0}:void 0}),x.addEventListener("contextmenu",i=>{const n=ee(i),o=n<0?void 0:s[n].key;o===void 0||!a.onWaferContextMenu||(B.style.display="none",a.onWaferContextMenu(o,f??void 0,i))}),E(),I=We(h,E)}return S(),{card:c,destroy:()=>{I?.disconnect(),B.remove(),c.remove()},setTest:l=>{J.set(l)&&(f=l,S())},setAxisPrefs:l=>{_.set(l)&&S()}}}
@@ -0,0 +1,13 @@
1
+ export interface ConfirmOptions {
2
+ doc: Document;
3
+ /** An element from the live DOM the dialog belongs to, so it lands inside a host's own `<dialog>` (see `OverlayOptions.anchor`). */
4
+ anchor: Element;
5
+ title: string;
6
+ /** What will happen, in a sentence or two. Plain text. */
7
+ message: string;
8
+ /** The destructive button's label, naming the action ("Delete 4 plots"), not "OK". */
9
+ confirmLabel: string;
10
+ cancelLabel?: string;
11
+ }
12
+ export declare function confirmDialog(o: ConfirmOptions): Promise<boolean>;
13
+ //# sourceMappingURL=confirmDialog.d.ts.map
@@ -0,0 +1 @@
1
+ import{openModal as u,controlStyle as y,wireControlHover as C,CLR as w,FONT as m,SPACE as i}from"./toolbar.js";export function confirmDialog(t){const{doc:o}=t;return new Promise(p=>{let s=!1,l;const n=u({title:t.title,anchor:t.anchor,ownerDocument:o,boxSize:{width:"min(90vw, 460px)",height:"auto"},maximizable:!1,initialFocus:()=>l,onClose:()=>p(s)});n.box.dataset.wmapConfirm="1",n.box.style.minHeight="0";const c=o.createElement("div");Object.assign(c.style,{display:"flex",flexDirection:"column",gap:i.lg,padding:i.xl,color:w.text,fontSize:m.body});const a=o.createElement("p");a.textContent=t.message,a.id=`wmap-confirm-${Math.random().toString(36).slice(2,8)}`,a.style.margin="0",n.box.setAttribute("aria-describedby",a.id);const r=o.createElement("div");Object.assign(r.style,{display:"flex",justifyContent:"flex-end",gap:i.md,flexWrap:"wrap"});const d=(f,g,x)=>{const e=o.createElement("button");return e.type="button",e.textContent=f,e.dataset[g]="1",Object.assign(e.style,{...y("outlined"),fontSize:m.body,padding:`${i.sm} ${i.lg}`}),C(e),e.addEventListener("click",x),e};l=d(t.cancelLabel??"Cancel","wmapConfirmCancel",()=>n.close());const b=d(t.confirmLabel,"wmapConfirmOk",()=>{s=!0,n.close()});r.append(l,b),c.append(a,r),n.contentWrap.appendChild(c)})}
@@ -0,0 +1,131 @@
1
+ import type { Die } from '../core/dies.js';
2
+ import { type MetadataFieldDef, type TestDef } from '../renderer/buildWaferMap.js';
3
+ import { type DieListDisplayOptions } from './dieList.js';
4
+ import { type SaveTextHandler } from './toolbar.js';
5
+ import type { InsightsItem } from './insightsTab.js';
6
+ import type { Wafer } from '../core/wafer.js';
7
+ export type DataView = 'statistics' | 'dies' | 'wafers';
8
+ /** What a table needs of a wafer. An `InsightsItem` fits; so does one wafer's share
9
+ * of a drilldown population, which has no `Wafer` object or stats of its own. */
10
+ export interface DataItem {
11
+ label: string;
12
+ dies: Die[];
13
+ waferIndex: number;
14
+ wafer?: Wafer;
15
+ /** The wafer's own pass bins: the Wafers view's yield judges by them. */
16
+ passBins: readonly number[];
17
+ statsSummary?: InsightsItem['statsSummary'];
18
+ }
19
+ export declare const DATA_VIEWS: ReadonlyArray<{
20
+ key: DataView;
21
+ label: string;
22
+ }>;
23
+ /** Rows Excel can open. A file with more is still valid; the reader is told. */
24
+ export declare const EXCEL_ROW_LIMIT = 1048576;
25
+ /** Cells above which Copy is not offered: a paste that size freezes the target. */
26
+ export declare const COPY_CELL_LIMIT = 200000;
27
+ /** Parametric tests given a mean column in the Wafers view. */
28
+ export declare const WAFER_TEST_COLUMNS = 50;
29
+ export interface DataSectionDeps {
30
+ doc: Document;
31
+ items: DataItem[];
32
+ /** Parametric tests of the scope. */
33
+ testDefs: TestDef[];
34
+ /** Every test of the scope, functional ones included. */
35
+ allTestDefs: TestDef[];
36
+ /** Active grouping, when any: Statistics then shows one block per group. */
37
+ groups?: {
38
+ key: string;
39
+ items: DataItem[];
40
+ }[];
41
+ groupLabelText?: string;
42
+ ringCount: number;
43
+ /** Lot yield per wafer index (the figure every other yield display uses). */
44
+ yieldByWaferIndex: ReadonlyMap<number, number | null>;
45
+ onSaveText?: SaveTextHandler;
46
+ /** The Statistics view's tables, built by the Insights tab (they are the
47
+ * test-values and functional cards that used to sit in Overview). */
48
+ buildStatistics: (items: DataItem[]) => {
49
+ cards: HTMLElement[];
50
+ destroy: () => void;
51
+ };
52
+ /** Which views to offer. Default all three. */
53
+ views?: readonly DataView[];
54
+ /** Host display choices for the Dies table (`RenderOptions.dieList`): which metadata columns to show. */
55
+ dieListOptions?: DieListDisplayOptions;
56
+ /** Label/order hints for die metadata columns, e.g. `WaferMapResult.metadataFields`. */
57
+ metadataFields?: MetadataFieldDef[];
58
+ /** A die row was clicked: show that die on its map. When set, Dies rows are clickable and say so. */
59
+ onLocateDie?: (die: Die, waferIndex: number | undefined) => void;
60
+ /** A wafer row was clicked: open that wafer. When set, Wafers rows are clickable and say so. */
61
+ onOpenWafer?: (waferIndex: number, label: string) => void;
62
+ /** Set when the tables describe part of a population (a drilldown selection): what to say after the
63
+ * counts ("selected on W03"), and a wafer column and file-name tag so a file says so too. */
64
+ population?: {
65
+ phrase: string;
66
+ fileTag: string;
67
+ };
68
+ view: DataView;
69
+ onViewChange: (view: DataView) => void;
70
+ /** The Dies view's Wide | Long choice. */
71
+ diesLayout: 'wide' | 'long';
72
+ onDiesLayoutChange: (layout: 'wide' | 'long') => void;
73
+ }
74
+ export declare function renderDataSection(deps: DataSectionDeps): {
75
+ card: HTMLElement;
76
+ destroy: () => void;
77
+ };
78
+ export interface DataTablesInput {
79
+ doc: Document;
80
+ /** One entry per wafer. `wafer` gives ring/quadrant and the lot/product columns; `passBins` and
81
+ * `statsSummary` give the Wafers view its yield and means. */
82
+ items: ReadonlyArray<{
83
+ label: string;
84
+ dies: Die[];
85
+ waferIndex?: number;
86
+ wafer?: Wafer;
87
+ passBins: readonly number[];
88
+ statsSummary?: DataItem['statsSummary'];
89
+ }>;
90
+ testDefs: TestDef[] | undefined;
91
+ /** Set when the tables describe part of a population — a drilldown selection: who the dies are, in
92
+ * words ("selected on W03"). A saved file then says so in its name and a Wafer column. Omit for a whole wafer or lot. */
93
+ population?: string;
94
+ view: DataView;
95
+ /** Which views to offer. Default Dies and Statistics, plus Wafers when there is more than one wafer. */
96
+ views?: readonly DataView[];
97
+ /** The wafers' ring count (`WaferMapResult.ringCount`): ring and quadrant columns use it. Required, never defaulted. */
98
+ ringCount: number;
99
+ metadataFields?: MetadataFieldDef[];
100
+ /** A die row clicked: show that die on its map. The caller closes the table around it. */
101
+ onLocateDie?: (die: Die, waferIndex: number | undefined) => void;
102
+ dieListOptions?: DieListDisplayOptions;
103
+ onSaveText?: SaveTextHandler;
104
+ }
105
+ /**
106
+ * The Data tab's tables for a population, in the same controls, for a modal —
107
+ * a drilldown selection, or a wafer or lot opened from the Summary panel. It does
108
+ * not need Insights to be enabled: it is built from the dies it is given.
109
+ * Statistics is the Test Values and Functional Tests tables over just those dies,
110
+ * stamped with their identity so a saved file says what it covers.
111
+ */
112
+ export declare function renderDataTables(input: DataTablesInput): {
113
+ el: HTMLElement;
114
+ destroy: () => void;
115
+ };
116
+ /** A drilldown population as tables: Dies and Statistics, named for who the dies are. */
117
+ export declare function renderSelectionTables(input: {
118
+ doc: Document;
119
+ /** The wafers' ring count (from their built results): ring and quadrant columns use it. */
120
+ ringCount: number;
121
+ items: DataTablesInput['items'];
122
+ testDefs: TestDef[] | undefined;
123
+ population: string;
124
+ view: DataView;
125
+ onSaveText?: SaveTextHandler;
126
+ onLocateDie?: DataTablesInput['onLocateDie'];
127
+ }): {
128
+ el: HTMLElement;
129
+ destroy: () => void;
130
+ };
131
+ //# sourceMappingURL=dataTab.d.ts.map
@@ -0,0 +1 @@
1
+ import{isYieldEligibleDie as W}from"../core/dies.js";import{testValue as O}from"../core/dieTable.js";import{metadataDisplayValue as I}from"../core/metadata.js";import{prettyKey as z}from"../core/utils.js";import{tableToTsv as N}from"../core/tableCsv.js";import{commonMetadata as H}from"../stats/facets.js";import{buildYieldData as Y}from"../stats/yield.js";import{isParametricTest as k}from"../renderer/buildWaferMap.js";import{fmt as G}from"../renderer/fmt.js";import{testLabel as R}from"../renderer/testLabel.js";import{resolveDieColumns as U}from"./dieList.js";import{createVirtualTable as V}from"./virtualTable.js";import{exportCsv as E}from"./tableExport.js";import{CLR as y,FONT as L,RADIUS as j,SPACE as C,controlStyle as K,wireControlHover as A,wireTooltip as $}from"./toolbar.js";import{buildTestSection as q,buildFunctionalTestSection as X}from"./summaryPanel.js";export const DATA_VIEWS=[{key:"statistics",label:"Statistics"},{key:"dies",label:"Dies"},{key:"wafers",label:"Wafers"}],EXCEL_ROW_LIMIT=1048576,COPY_CELL_LIMIT=2e5,WAFER_TEST_COLUMNS=50;const D=(e,i,s=i+"s")=>`${e.toLocaleString()} ${e===1?i:s}`;export function renderDataSection(e){const{doc:i}=e,s=i.createElement("div");s.dataset.wmapDataTab="1",Object.assign(s.style,{display:"flex",flexDirection:"column",gap:C.md,minWidth:"0"});const c=i.createElement("div");Object.assign(c.style,{display:"flex",alignItems:"center",gap:C.md,flexWrap:"wrap"}),s.appendChild(c);const p=i.createElement("div");Object.assign(p.style,{display:"flex",flexDirection:"column",gap:C.md,minWidth:"0"}),s.appendChild(p);const g=DATA_VIEWS.filter(f=>!e.views||e.views.includes(f.key)),r=F(i,"Table",g,e.view,"wmapDataView",f=>e.onViewChange(f));c.appendChild(r.el);let d;switch(e.view){case"dies":d=Q(e);break;case"wafers":d=Z(e);break;default:d=J(e)}let u=()=>{};if(e.view==="dies"){const f=F(i,"Export format",[{key:"wide",label:"Wide"},{key:"long",label:"Long"}],e.diesLayout,"wmapDataLayout",t=>{f.set(t),e.onDiesLayoutChange(t),d.actions?.setLayout?.(t),u()});$(f.el,"Wide: a column per test. Long: a row per die per test, for tools that want tidy data. The table on screen is always wide.");const l=i.createElement("span");Object.assign(l.style,{display:"inline-flex",alignItems:"center",gap:C.sm});const o=i.createElement("span");o.textContent="Export format:",Object.assign(o.style,{color:y.label,fontSize:L.body}),l.append(o,f.el),c.appendChild(l)}if(d.actions){const f=d.actions,l=i.createElement("div");l.style.flex="1",c.appendChild(l);const o=B(i,"Export CSV");o.dataset.wmapDataExport="1",o.addEventListener("click",()=>{if(o.disabled)return;const m=o.textContent,h=f.exportCsv();h&&(o.disabled=!0,o.setAttribute("aria-busy","true"),h.catch(()=>{}).finally(()=>{o.disabled=!1,o.removeAttribute("aria-busy"),o.textContent=m}))});const t=B(i,"Copy");t.dataset.wmapDataCopy="1",f.copyText?($(t,"Copy the table as tab-separated text, to paste into a spreadsheet"),t.addEventListener("click",()=>{const m=f.copyText(),h=i.defaultView?.navigator?.clipboard;if(!h)return;const w=t.textContent;h.writeText(m).then(()=>{t.textContent="Copied",i.defaultView?.setTimeout(()=>{t.textContent=w},1500)}).catch(()=>{})})):(t.disabled=!0,$(t,`Too large to copy (over ${COPY_CELL_LIMIT.toLocaleString()} cells). Use Export CSV.`)),c.append(t,o);const a=i.createElement("div");a.dataset.wmapDataNote="1",a.textContent=f.note(),u=()=>{a.textContent=f.note()},Object.assign(a.style,{color:y.label,fontSize:L.body}),s.insertBefore(a,p)}return p.appendChild(d.el),{card:s,destroy:()=>d.destroy()}}function J(e){const{doc:i}=e,s=i.createElement("div");Object.assign(s.style,{display:"flex",flexDirection:"column",gap:C.lg,minWidth:"0"});const c=[],p=e.groups&&e.groups.length>0?e.groups:void 0,g=p??[{key:"",items:e.items}];for(const r of g){if(p){const u=i.createElement("div");u.textContent=`${e.groupLabelText??"Group"}: ${r.key}`,Object.assign(u.style,{fontWeight:"700",color:y.text,fontSize:L.body}),s.appendChild(u)}const d=e.buildStatistics(r.items);c.push(d.destroy);for(const u of d.cards)s.appendChild(u)}return!s.querySelector("table")&&!s.textContent&&(s.textContent="No test statistics to show for these wafers.",Object.assign(s.style,{color:y.label,fontSize:L.body})),{el:s,destroy:()=>{for(const r of c)r()}}}function Q(e){const{doc:i,items:s}=e,c=new WeakMap,p=new WeakMap,g=new WeakMap,r=[];let d=!1;for(const n of s){n.wafer&&(d=!0);for(const b of n.dies)c.set(b,n.label),g.set(b,n.waferIndex),n.wafer&&p.set(b,n.wafer),r.push(b)}if(r.length===0)return M(i,"No dies to show.");const{columns:u,visibleColumns:f}=U(r,e.allTestDefs,{...e.dieListOptions,metadataFields:e.metadataFields,extraColumn:s.length>1||e.population?{label:"Wafer",get:n=>c.get(n)}:void 0,getWafer:d?(n=>p.get(n)):void 0,ringCount:e.ringCount,waferMetadata:s.some(n=>n.wafer)?H(s.map(n=>({metadata:n.wafer?.metadata}))):void 0,ownerDocument:i}),l=f.map(n=>({header:n.label,get:n.get,numeric:n.test!==void 0&&k(n.test),...n.test!==void 0&&k(n.test)?{sortKey:b=>O(b,n.test.testNumber)}:{}})),o=V({columns:l,rows:r,ariaLabel:`Dies (${r.length})`,ownerDocument:i,onRowClick:e.onLocateDie?n=>e.onLocateDie(n,g.get(n)):void 0});o.el.style.maxHeight="70vh",o.el.style.minHeight="240px";const t=e.population?`-${e.population.fileTag}`:"",a=u.filter(n=>n.test!==void 0),m=u.filter(n=>n.test===void 0),h=r.length*u.length,w=r.length*a.length,x=()=>u.map(n=>({header:n.csvLabel??n.label,get:n.csvGet??n.get})),v=()=>[...m.map(n=>({header:n.csvLabel??n.label,get:b=>(n.csvGet??n.get)(b.die)})),{header:"Test",get:n=>R(n.test,n.test.testNumber)},{header:"Test number",get:n=>n.test.testNumber},{header:"Unit",get:n=>n.test.unit??""},{header:"Value",get:n=>n.value}];function*_(){for(const n of o.orderedRows())for(const b of a){const S=(b.csvGet??b.get)(n);S===""||S===void 0||S===null||(yield{die:n,test:b.test,value:S})}}let T=e.diesLayout==="long"&&a.length>0;const P=()=>T?`Long format: up to ${D(w,"row")} (a die with no result for a test has no row)`+(w>EXCEL_ROW_LIMIT?` \u2014 more than Excel opens (${EXCEL_ROW_LIMIT.toLocaleString()}).`:"."):`Wide format: ${D(r.length,"row")} \xD7 ${D(u.length,"column")}`+(r.length>EXCEL_ROW_LIMIT?` \u2014 more than Excel opens (${EXCEL_ROW_LIMIT.toLocaleString()} rows).`:".");return{el:o.el,destroy:()=>o.destroy(),actions:{note:()=>`${D(r.length,"die")} ${e.population?.phrase??`on ${D(s.length,"wafer")}`}. ${P()} Export follows the order shown.${e.onLocateDie?" Click a row to show that die on the map.":""}`,setLayout:n=>{T=n==="long"&&a.length>0},exportCsv:()=>T?E(v(),_(),`die-list-long${t}.csv`,e.onSaveText,{rowCount:w}):E(x(),o.orderedRows(),`die-list${t}.csv`,e.onSaveText,{rowCount:r.length}),copyText:h<=COPY_CELL_LIMIT?()=>N(x(),o.orderedRows()):null}}}function Z(e){const{doc:i,items:s}=e;if(s.length===0)return M(i,"No wafers to show.");const c=s.map(t=>{const a=e.yieldByWaferIndex.get(t.waferIndex),m=Y([{label:t.label,dies:t.dies,passBins:t.passBins,yieldPercent:a,key:t.waferIndex}])[0]?.percent;return{item:t,yieldPercent:t.dies.length?m??null:null}}),p=[];for(const{item:t}of c)for(const a of Object.keys(t.wafer?.metadata??{}))!p.includes(a)&&I((t.wafer?.metadata)[a])!==void 0&&p.push(a);const g=e.testDefs.slice(0,WAFER_TEST_COLUMNS),r=(t,a)=>{const m=t.item.statsSummary?.stats.perTestStats?.find(x=>x.testNumber===a.testNumber);if(m)return m.mean;let h=0,w=0;for(const x of t.item.dies){if(!W(x))continue;const v=O(x,a.testNumber);v!==void 0&&Number.isFinite(v)&&(h++,w+=v)}return h?w/h:void 0},d=[{header:"Wafer",cell:t=>t.item.label},...p.map(t=>({header:z(t),cell:a=>I(a.item.wafer?.metadata?.[t])??""})),{header:"Dies",cell:t=>t.item.dies.length,numeric:!0},{header:"Yield-eligible dies",cell:t=>t.item.dies.filter(a=>W(a)).length,numeric:!0},{header:"Yield %",cell:t=>t.yieldPercent===null?"":t.yieldPercent,show:t=>t.yieldPercent===null?"":t.yieldPercent.toFixed(1),numeric:!0},...g.map(t=>({header:`${R(t,t.testNumber)}${t.unit?` (${t.unit})`:""} mean`,cell:a=>r(a,t),show:a=>{const m=r(a,t);return m===void 0?"":G(m,t.unit)},numeric:!0}))],u=d.map(t=>({header:t.header,get:a=>t.show?t.show(a):String(t.cell(a)??""),numeric:t.numeric,...t.numeric?{sortKey:a=>{const m=t.cell(a);return typeof m=="number"?m:void 0}}:{}})),f=V({columns:u,rows:c,ariaLabel:`Wafers (${c.length})`,ownerDocument:i,onRowClick:e.onOpenWafer?t=>e.onOpenWafer(t.item.waferIndex,t.item.label):void 0});f.el.style.maxHeight="70vh",f.el.style.minHeight="120px";const l=()=>d.map(t=>({header:t.header,get:t.cell})),o=e.testDefs.length>g.length?` Mean columns cover the first ${g.length} of ${e.testDefs.length} tests.`:"";return{el:f.el,destroy:()=>f.destroy(),actions:{note:()=>`${D(c.length,"wafer")}.${o}${e.onOpenWafer?" Click a row to open that wafer.":""} Export follows the order shown.`,exportCsv:()=>E(l(),f.orderedRows(),"wafers.csv",e.onSaveText,{rowCount:c.length}),copyText:c.length*d.length<=COPY_CELL_LIMIT?()=>N(l(),f.orderedRows()):null}}}function M(e,i){const s=e.createElement("div");return s.textContent=i,Object.assign(s.style,{color:y.label,fontSize:L.body,padding:C.md}),{el:s,destroy:()=>{}}}function B(e,i){const s=e.createElement("button");return s.type="button",s.textContent=i,Object.assign(s.style,{fontSize:L.body,padding:"3px 10px",borderRadius:j.control,...K("outlined"),background:y.menuBg}),A(s),s}function F(e,i,s,c,p,g){const r=e.createElement("div");r.setAttribute("role","radiogroup"),r.setAttribute("aria-label",i),Object.assign(r.style,{display:"inline-flex",background:y.menuBg,border:`1px solid ${y.menuBorder}`,borderRadius:j.control,overflow:"hidden"});const d=[];let u=c;const f=()=>{for(const l of d){const o=l.dataset[p]===u;l.setAttribute("aria-checked",String(o)),l.tabIndex=o?0:-1,Object.assign(l.style,{background:o?y.bgActive:"transparent",color:o?y.iconActive:y.label,fontWeight:o?"600":"400"})}};for(const l of s){const o=e.createElement("button");o.type="button",o.textContent=l.label,o.dataset[p]=l.key,o.setAttribute("role","radio"),Object.assign(o.style,{border:"none",cursor:"pointer",fontSize:L.body,padding:`${C.xs} ${C.md}`,whiteSpace:"nowrap"}),A(o,"bare"),o.addEventListener("click",()=>{u!==l.key&&g(l.key)}),d.push(o),r.appendChild(o)}return f(),r.addEventListener("keydown",l=>{if(l.key!=="ArrowLeft"&&l.key!=="ArrowRight")return;const o=d.indexOf(e.activeElement);if(o<0)return;l.preventDefault();const t=d[(o+(l.key==="ArrowRight"?1:-1)+d.length)%d.length];t.click(),t.focus()}),{el:r,set:l=>{u=l,f()}}}export function renderDataTables(e){const{doc:i}=e,s=i.createElement("div");Object.assign(s.style,{display:"flex",flexDirection:"column",minWidth:"0",minHeight:"0",flex:"1",padding:C.md});const c=e.items.map((a,m)=>({...a,waferIndex:a.waferIndex??m})),p=c.flatMap(a=>a.dies),g=e.testDefs??[],r=g.filter(k),d=e.views??(c.length>1?["statistics","dies","wafers"]:["statistics","dies"]),u=e.population?{populationLabel:`${D(p.length,"die")} ${e.population}`}:c.length>1?{perWaferMetadata:c.map(a=>a.wafer?.metadata??{}),populationLabel:`${D(c.length,"wafer")} pooled`}:{waferMetadata:c[0]?.wafer?.metadata};let f=d.includes(e.view)?e.view:d[0],l="wide",o=null;const t=()=>{o?.destroy(),o=renderDataSection({doc:i,items:c,testDefs:r,allTestDefs:g,ringCount:e.ringCount,yieldByWaferIndex:new Map,onSaveText:e.onSaveText,views:d,view:f,dieListOptions:e.dieListOptions,metadataFields:e.metadataFields,onLocateDie:e.onLocateDie,population:e.population?{phrase:e.population,fileTag:"selection"}:void 0,buildStatistics:a=>{const m=a.flatMap(v=>v.dies),h=[],w=q(m,r,void 0,void 0,e.onSaveText,u,a.map(v=>({dies:v.dies})),void 0,"full");w&&h.push(w);const x=X(m,g,void 0,e.onSaveText,u);return x&&h.push(x),{cards:h,destroy:()=>{}}},onViewChange:a=>{f=a,t()},diesLayout:l,onDiesLayoutChange:a=>{l=a}}),s.replaceChildren(o.card)};return t(),{el:s,destroy:()=>o?.destroy()}}export function renderSelectionTables(e){return renderDataTables({...e,views:e.items.length>1?["statistics","dies","wafers"]:["statistics","dies"]})}
@@ -4,6 +4,7 @@ import { type MetadataFieldDef, type TestDef } from '../renderer/buildWaferMap.j
4
4
  import type { WaferMetadata } from '../core/metadata.js';
5
5
  import { type MetadataKeySelection } from '../stats/metadataColumns.js';
6
6
  import { type SaveTextHandler } from './toolbar.js';
7
+ import { type CsvCell } from './tableExport.js';
7
8
  /** Display preferences for the built-in die-list table. Everything here is a
8
9
  * choice about what to show; the *data* it acts on (wafer metadata, metadata
9
10
  * field definitions) is supplied by the library from its own build result
@@ -12,10 +13,12 @@ import { type SaveTextHandler } from './toolbar.js';
12
13
  * identity data by mistake. */
13
14
  export interface DieListDisplayOptions {
14
15
  /**
15
- * Show a "View die list" link inside the Summary panel (`renderWaferMap`)
16
+ * Show a "Data tables" button inside the Summary panel (`renderWaferMap`)
16
17
  * / lot Summary panel (`renderWaferGallery`) that opens this wafer's — or,
17
- * in the gallery, the whole lot's — dies as a table in a modal, with the
18
- * same "Export CSV" button every die list has. **Default
18
+ * in the gallery, the whole lot's — tables in a modal: Statistics and Dies
19
+ * (and, for a lot, Wafers), each with Export CSV and Copy. The Dies table is
20
+ * virtual, so `maxRows` does not apply to it. Works whether or not Insights
21
+ * is enabled. **Default
19
22
  * `true`** — set `enabled: false` to hide it. Requires a Summary panel to
20
23
  * be reachable at all (`RenderOptions.summaryPanel` / `GalleryOptions.summaryPanel`),
21
24
  * since that panel is this link's only home; irrelevant otherwise.
@@ -24,7 +27,8 @@ export interface DieListDisplayOptions {
24
27
  * without position" footer, both of which show a die list unconditionally
25
28
  * regardless of this flag — those exist because there is literally nothing
26
29
  * else to show, not as an opt-in extra. The rest of this options object
27
- * (column selection, `maxRows`, …) is shared by both surfaces.
30
+ * (column selection, `maxRows`, …) is shared by those two surfaces and the modal's Dies table
31
+ * (which ignores `maxRows`).
28
32
  */
29
33
  enabled?: boolean;
30
34
  /**
@@ -123,6 +127,37 @@ export interface DieListOptions extends DieListDisplayOptions {
123
127
  */
124
128
  ownerDocument?: Document;
125
129
  }
130
+ /** One scoped `<style>` block for die-list cells, shared by every table
131
+ * instance, instead of an inline style object per `<td>`/`<th>`. At lot
132
+ * scale (hundreds of thousands of dies) that is the difference between one
133
+ * style recalculation and one Object.assign per cell — a real cost when the
134
+ * table already has no virtualisation. CLR's tokens are `var(--wmap-*, …)`
135
+ * strings, so they resolve identically in a stylesheet rule as inline.
136
+ * Injected into `doc` specifically — see `stylesInjectedInto`'s comment. */
137
+ export declare function ensureStylesInjected(doc: Document): void;
138
+ export type DieColumn = {
139
+ label: string;
140
+ /** CSV header when it must differ from the on-screen one — a derived test's
141
+ * column, where the screen shows the † mark and the file, which has no key
142
+ * to explain a glyph, states what it was derived from. */
143
+ csvLabel?: string;
144
+ get: (die: Die) => string;
145
+ /** CSV rendering when it must differ from the on-screen cell — see the test
146
+ * columns, where the screen shows `300 mV` and the file a bare `300`. */
147
+ csvGet?: (die: Die) => CsvCell;
148
+ csvOnly?: boolean;
149
+ /** Set on a test's column — what the long-format export (one row per die per test) unpivots. */
150
+ test?: TestDef;
151
+ };
152
+ /** The columns of a die table — what the screen shows, and (where it differs)
153
+ * what the file carries — for `dies`. Shared by the die-list modal and the Data
154
+ * tab's Dies view, so the two cannot describe a die differently. */
155
+ export declare function resolveDieColumns(dies: Die[], testDefs: TestDef[] | undefined, options?: DieListOptions): {
156
+ columns: DieColumn[];
157
+ visibleColumns: DieColumn[];
158
+ testColumns: TestDef[];
159
+ truncatedKeys: string[];
160
+ };
126
161
  /**
127
162
  * Build a scrollable die-list table + "Export CSV" button. Returns `null`
128
163
  * when `dies` is empty (nothing to show).