@wafertools/wafermap 0.26.1 → 0.27.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 (88) hide show
  1. package/CHANGELOG.md +360 -0
  2. package/README.md +46 -3
  3. package/dist/packages/canvas-adapter/canvasTheme.d.ts +4 -0
  4. package/dist/packages/canvas-adapter/canvasTheme.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +16 -0
  6. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +13 -1
  9. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/capability.d.ts +10 -10
  11. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +337 -3
  13. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/correlation.d.ts +8 -9
  15. package/dist/packages/canvas-adapter/charts/correlation.js +2 -1
  16. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +50 -0
  17. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -0
  18. package/dist/packages/canvas-adapter/charts/histogram.d.ts +12 -1
  19. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  20. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  21. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  22. package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +20 -0
  23. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -0
  24. package/dist/packages/canvas-adapter/charts/trend.d.ts +30 -0
  25. package/dist/packages/canvas-adapter/charts/trend.js +1 -0
  26. package/dist/packages/canvas-adapter/dieList.js +7 -7
  27. package/dist/packages/canvas-adapter/identityHeader.d.ts +46 -0
  28. package/dist/packages/canvas-adapter/identityHeader.js +1 -0
  29. package/dist/packages/canvas-adapter/index.d.ts +1 -1
  30. package/dist/packages/canvas-adapter/insightsTab.d.ts +27 -3
  31. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  32. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  33. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +15 -2
  34. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -20
  35. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +72 -17
  36. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  37. package/dist/packages/canvas-adapter/summaryPanel.d.ts +219 -33
  38. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  39. package/dist/packages/canvas-adapter/toCanvas.d.ts +6 -0
  40. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  41. package/dist/packages/canvas-adapter/toolbar.d.ts +310 -6
  42. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  43. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  44. package/dist/packages/canvas-adapter/userGuideHtml.js +97 -34
  45. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  46. package/dist/packages/canvas-adapter/version.js +1 -1
  47. package/dist/packages/canvas-adapter/warnings.d.ts +0 -4
  48. package/dist/packages/canvas-adapter/warnings.js +1 -1
  49. package/dist/packages/core/utils.d.ts +11 -0
  50. package/dist/packages/core/utils.js +1 -1
  51. package/dist/packages/renderer/buildView.d.ts +34 -1
  52. package/dist/packages/renderer/buildView.js +1 -1
  53. package/dist/packages/renderer/buildWaferMap.d.ts +104 -5
  54. package/dist/packages/renderer/buildWaferMap.js +1 -1
  55. package/dist/packages/renderer/colorSchemes.js +1 -1
  56. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  57. package/dist/packages/stats/binPareto.d.ts +15 -0
  58. package/dist/packages/stats/binPareto.js +1 -1
  59. package/dist/packages/stats/clusterDetection.js +1 -1
  60. package/dist/packages/stats/connectedComponents.d.ts +14 -0
  61. package/dist/packages/stats/connectedComponents.js +1 -0
  62. package/dist/packages/stats/correlation.d.ts +26 -0
  63. package/dist/packages/stats/correlation.js +1 -1
  64. package/dist/packages/stats/facets.js +1 -1
  65. package/dist/packages/stats/filterFindings.d.ts +18 -0
  66. package/dist/packages/stats/filterFindings.js +1 -1
  67. package/dist/packages/stats/findingsNarrative.js +1 -1
  68. package/dist/packages/stats/histogram.d.ts +19 -1
  69. package/dist/packages/stats/histogram.js +1 -1
  70. package/dist/packages/stats/index.d.ts +6 -0
  71. package/dist/packages/stats/index.js +1 -1
  72. package/dist/packages/stats/mergeTestDefs.d.ts +44 -0
  73. package/dist/packages/stats/mergeTestDefs.js +1 -0
  74. package/dist/packages/stats/metadataColumns.d.ts +20 -4
  75. package/dist/packages/stats/metadataColumns.js +1 -1
  76. package/dist/packages/stats/patternClassification.js +1 -1
  77. package/dist/packages/stats/renderFindingsReport.js +14 -14
  78. package/dist/packages/stats/renderSummaryReport.js +32 -29
  79. package/dist/packages/stats/reportHtml.js +38 -13
  80. package/dist/packages/stats/testPassRate.d.ts +85 -0
  81. package/dist/packages/stats/testPassRate.js +1 -0
  82. package/dist/packages/stats/trend.d.ts +40 -0
  83. package/dist/packages/stats/trend.js +1 -0
  84. package/dist/packages/stats/types.d.ts +117 -6
  85. package/llms.txt +6 -0
  86. package/package.json +7 -5
  87. package/dist/packages/canvas-adapter/metadataBadge.d.ts +0 -30
  88. package/dist/packages/canvas-adapter/metadataBadge.js +0 -1
@@ -1 +1 @@
1
- import{findTestDef as xe,buildMapTitle as ye}from"../renderer/buildView.js";import{affineInvert as Oe,affineVector as Me}from"../core/transforms.js";import{compareNatural as Ee}from"../core/utils.js";import{getColorScheme as be}from"../renderer/colorSchemes.js";import{SPEC_PASS_FILL as Te,SPEC_FAIL_LOW as Ye,SPEC_FAIL_HIGH as Se,contrastTextColor as ke}from"../renderer/colorMap.js";import{fmt as Le,fmtColorbarAxis as we}from"../renderer/fmt.js";import{resolveCanvasTheme as Xe}from"./canvasTheme.js";function Ae(g,e,N,M,h){g.moveTo(e,N+h*M),g.lineTo(e-M*.9,N-h*M*.6),g.lineTo(e+M*.9,N-h*M*.6),g.closePath()}const He=new Set(["value","stackedValues","stackedBins","stackedSoftBins"]),$e=new Set(["hardBin","softBin","metadata"]),v="10px system-ui, sans-serif",Ge="600 12px system-ui, sans-serif",Gt="11px system-ui, sans-serif",Ve="600 11px system-ui, sans-serif",Vt=128,Ue="10px system-ui, sans-serif",Re=4,Y=17,X=11;export const BIN_LEGEND_W=110,BIN_LEGEND_W_COMPACT=64,BIN_LEGEND_ADAPT_COMPACT=280,BIN_LEGEND_ADAPT_FLOATING=180,BIN_LEGEND_MIN_CANVAS_W=150,BIN_LEGEND_MIN_CANVAS_H=120;const Ut=28,gt=5,mt=8,qe=15;export function toCanvas(g,e,N={}){const M=Xe(g.parentElement??g),{padding:h=16,showColorbar:$=!0,markFailingDies:G=!1,colorbarWidth:_=16,background:ut=M.background,legendPosition:Z="default",legendOffset:pt,showAxes:xt=!1,diePitchMm:V,viewport:K,activeBin:E,fallbackFormat:z,topClearance:j=0,minRightReserve:kt,hbinDefs:at,sbinDefs:yt,metadataFields:Mt,showTitle:Q=!0}=N,I=Math.floor(g.clientWidth||g.width),H=Math.floor(g.clientHeight||g.height),bt=I>=BIN_LEGEND_MIN_CANVAS_W&&H>=BIN_LEGEND_MIN_CANVAS_H,Lt=$&&bt&&He.has(e.plotMode)&&e.passFailDisplay==="off",Tt=$&&bt&&$e.has(e.plotMode),tt=$&&bt&&e.plotMode==="value"&&(e.passFailDisplay==="spec"&&e.specCounts!==void 0||e.passFailDisplay==="test"&&e.passFailCounts!==void 0),et=Tt||tt,At=Tt&&e.plotMode!=="metadata"&&e.binCounts?[...e.binCounts.entries()].sort(([o],[r])=>o-r):[],Rt=Tt&&e.plotMode==="metadata"&&e.metadataCounts?[...e.metadataColorMap?.keys()??[...e.metadataCounts.keys()].sort(Ee)].map(o=>[o,e.metadataCounts.get(o)??0]):[],ot=[];if(tt&&e.passFailDisplay==="test"){const o=e.passFailCounts;ot.push({key:-1,color:Te,label:"Pass",compactLabel:"Pass",count:o.pass,tooltip:`Pass \xB7 ${o.pass} dies`}),ot.push({key:-2,color:Se,label:"Fail",compactLabel:"Fail",count:o.fail,tooltip:`Fail \xB7 ${o.fail} dies`})}else if(tt){const o=e.specCounts,r=xe(e.testDefs,e.activeTest);ot.push({key:-1,color:Te,label:"Pass",compactLabel:"Pass",count:o.pass,tooltip:`Pass \xB7 ${o.pass} dies`}),r?.limitHigh!==void 0&&ot.push({key:-2,color:Se,label:"Fail high",compactLabel:"Fail hi",count:o.failHigh,tooltip:`Fail high \xB7 ${o.failHigh} dies`}),r?.limitLow!==void 0&&ot.push({key:-3,color:Ye,label:"Fail low",compactLabel:"Fail lo",count:o.failLow,tooltip:`Fail low \xB7 ${o.failLow} dies`})}const Ct=tt?ot.length:e.plotMode==="metadata"?Rt.length:At.length,m=(g.ownerDocument.defaultView??window).devicePixelRatio??1;if(I<=0||H<=0)return{hitTarget:{getDieAtPoint:()=>null},viewport:{originX:0,originY:0,ppm:1,snapDist:1},binLegendRows:[]};const C=Z!=="default"?Z:I<BIN_LEGEND_ADAPT_FLOATING?"floating":I<BIN_LEGEND_ADAPT_COMPACT?"compact":"default",dt=C==="default"||C==="compact",ne=C==="left",zt=C==="bottom",_t=C==="top",Pt=C==="floating",Wt=C==="compact"||C==="floating"?BIN_LEGEND_W_COMPACT:BIN_LEGEND_W,se=Math.floor((H-2*h)/Y),le=Ct>se?se-1:Ct,ie=et&&zt?le*Y:0,Kt=et&&_t?le*Y:0,Jt=Lt?_+28:et&&dt?Wt:0,Zt=et&&ne?Wt:0,jt=xt?32:0,Bt=xt?36:0,Ce=kt!=null?Math.max(Jt,kt):Jt,_e=I-2*h-Ce-Bt-Zt,ae=H-2*h-jt-ie-Kt;g.width=Math.round(I*m),g.height=Math.round(H*m);const t=g.getContext("2d");t.scale(m,m),t.fillStyle=ut,t.fillRect(0,0,I,H);const U=e.hoverPoints;if(!U.length)return{hitTarget:{getDieAtPoint:()=>null},viewport:{originX:0,originY:0,ppm:1,snapDist:1},binLegendRows:[]};const Dt=e.rectangles[0],Qt=Dt?Dt.width/2:0,vt=Dt?Dt.height/2:0,Ft=e.dieBounds,ce=(Ft?.minX??0)-Qt,Pe=(Ft?.maxX??0)+Qt,We=(Ft?.minY??0)-vt,re=(Ft?.maxY??0)+vt,fe=Pe-ce,he=re-We;let nt,st,b;if(K)({originX:nt,originY:st,ppm:b}=K);else{const r=e.notchDir,l=r&&r.y>.1?13:0,a=r&&r.y<-.1?13:0,c=r&&r.x<-.1?13:0,f=r&&r.x>.1?13:0,d=_e-c-f,i=ae-j-l-a;b=Math.min(d/fe,i/he),nt=h+Bt+Zt+c+(d-fe*b)/2-ce*b,st=h+j+Kt+l+(i-he*b)/2+re*b}const It=K?.snapDist??Math.max(Qt,vt,1)*1.5,Nt=nt+e.waferCenter.x*b,Be=st-e.waferCenter.y*b,te=e.waferRadius*b,ct=o=>{const r=Math.abs(o-Be);return r>=te?0:Math.sqrt(te*te-r*r)},rt=(o,r,l,a,c,f,d=Ge,i=M.text)=>{if(!o)return;t.save(),t.fillStyle=i,t.font=d,t.textAlign=a,t.textBaseline=c;const p=a==="right"?r-f:f-r;let s=o;if(p>8&&t.measureText(s).width>p){for(;s.length>1&&t.measureText(s+"\u2026").width>p;)s=s.slice(0,-1);s=s+"\u2026"}p>8&&t.fillText(s,r,l),t.restore()};if(t.save(),t.setTransform(b*m,0,0,-b*m,nt*m,st*m),e.rectangles.length>0){const o=new Map;for(const l of e.rectangles){const a=String(l.fill);let c=o.get(a);c||(c=[],o.set(a,c)),c.push(l)}for(const[l,a]of o){t.beginPath();for(const c of a)t.rect(c.x-c.width/2,c.y-c.height/2,c.width,c.height);t.fillStyle=l,t.fill()}t.strokeStyle="rgba(0,0,0,0.18)",t.lineWidth=.5/(b*m),t.beginPath();for(const l of e.rectangles)t.rect(l.x-l.width/2,l.y-l.height/2,l.width,l.height);if(t.stroke(),G){const l=e.rectangles.filter(a=>a.binFail);if(l.length>0){const a=new Map;for(const c of l){const f=ke(String(c.fill));let d=a.get(f);d||(d=[],a.set(f,d)),d.push(c)}t.save(),t.lineCap="butt";for(const[c,f]of a){t.strokeStyle=c,t.globalAlpha=.55,t.lineWidth=Math.max(.6/(b*m),Math.min(f[0].width,f[0].height)*.1);for(const d of f){const i=d.width/2,p=d.height/2,s=Math.max(1.6/(b*m),Math.min(d.width,d.height)/3);t.save(),t.beginPath(),t.rect(d.x-i,d.y-p,d.width,d.height),t.clip(),t.beginPath();for(let x=-(i+p);x<=i+p;x+=s)t.moveTo(d.x-i+x,d.y-p),t.lineTo(d.x-i+x+d.height,d.y+p);t.stroke(),t.restore()}}t.restore()}}const r=e.rectangles.filter(l=>l.specMark);if(r.length>0){const l=new Map;for(const c of r){const f=ke(String(c.fill)),d=`${c.specMark}|${f}`;let i=l.get(d);i||(i=[],l.set(d,i)),i.push(c)}const a=2.5/(b*m);t.lineJoin="round";for(const[c,f]of l){const[d,i]=c.split("|"),p=i==="#000000"?"#ffffff":"#000000",s=d==="failHigh"?1:-1,x=Math.min(f[0].width,f[0].height),w=Math.max(2.5/(b*m),x*.3);t.beginPath();for(const k of f)Ae(t,k.x,k.y,w,s);t.strokeStyle=p,t.lineWidth=a,t.stroke(),t.fillStyle=i,t.fill()}}}for(const o of e.overlays){t.beginPath();for(const r of o.points){for(let l=0;l<r.length;l++)l===0?t.moveTo(r[l].x,r[l].y):t.lineTo(r[l].x,r[l].y);o.closed&&t.closePath()}o.fill&&!o.fill.startsWith("rgba(0,0,0,0)")&&(t.fillStyle=o.fill,t.fill()),o.kind==="ring-boundary"||o.kind==="quadrant-boundary"||o.kind==="reticle"?(t.strokeStyle="rgba(0,0,0,0.45)",t.lineWidth=3/(b*m),t.stroke(),t.strokeStyle="rgba(255,255,255,0.8)",t.lineWidth=1/(b*m),t.stroke()):(t.strokeStyle=o.lineColor,t.lineWidth=o.lineWidth/(b*m),t.stroke())}if(t.restore(),e.notchDir){const o=nt+e.waferCenter.x*b,r=st-e.waferCenter.y*b,l=e.notchDir.x,a=-e.notchDir.y,c=e.waferRadius*b,f=4,d=9,i=5,p=o+l*(c+f),s=r+a*(c+f),x=p+l*d,w=s+a*d,k={x:-a,y:l},B=h+Bt+Zt,lt=I-h-Jt,q=h+j+Kt,ht=H-h-ie-jt;t.save(),t.beginPath(),t.rect(B,q,lt-B,ht-q),t.clip(),t.beginPath(),t.moveTo(p,s),t.lineTo(x+k.x*i,w+k.y*i),t.lineTo(x-k.x*i,w-k.y*i),t.closePath(),t.fillStyle=M.text,t.fill(),t.restore()}t.save(),t.textAlign="center",t.textBaseline="middle";let de=0;if(e.rectangles.length>0){let o=1;for(const f of e.texts)f.role!=="indicator"&&f.text.length>o&&(o=f.text.length);const r=e.rectangles[0].width*b,l=e.rectangles[0].height*b,a=Math.min(r,l),c=r/o;de=Math.max(8,Math.min(64,Math.round(Math.min(a*.55,c*1.8))))}for(const o of e.texts){const r=nt+o.x*b,l=st-o.y*b;t.font=`${o.role==="indicator"?o.fontSize:de}px system-ui, sans-serif`,t.fillStyle=o.color,t.fillText(o.text,r,l)}if(t.restore(),xt&&ze(t,I,H,nt,st,b,h,jt,Bt,V,e.gridToScreen,e.rotation,M),Lt){const o=be(e.colorScheme),r=20,l=ae-j,a=Math.round(l*.75),c=h+j+Math.round((l-a)/2),f=I-h-_-r,[d,i]=e.valueRange,p=i-d,s=e.plotMode==="stackedSoftBins"?yt:at,{primary:x,secondary:w}=Q?ye(e,z,s):{primary:"",secondary:""};if(Q){const n=Math.max(c-6,h+j+11),u=Nt+Math.max(ct(n),ct(n-12))+8;rt(x,I-h,n,"right","bottom",u)}let k=c+a+4;const B=n=>Nt+Math.max(ct(n),ct(n+12))+8,lt=e.logScale&&d>0?Math.log10(d):0,q=e.logScale&&i>0?Math.log10(i)-lt:1,ht=n=>e.logScale?(1-(Math.log10(n)-lt)/q)*a:(1-(n-d)/p)*a;for(let n=0;n<Vt;n++){const u=1-n/(Vt-1),D=c+n/Vt*a,W=a/Vt+1;t.fillStyle=o.forValue(u),t.fillRect(f,D,_,W)}t.strokeStyle="rgba(0,0,0,0.25)",t.lineWidth=.5,t.strokeRect(f,c,_,a),t.fillStyle=M.text,t.font=v,t.textAlign="left",t.strokeStyle="rgba(0,0,0,0.35)",t.lineWidth=.5;const L=3,O=36,it=14,Xt=e.logScale&&q>0?Ke(d,i,a,O,it,lt,q):e.allIntegerValues?(()=>{const n=Math.max(1,qt(p*O/a)),u=[],D=Math.ceil(d/n)*n;for(let W=D;W<=i+n*1e-6;W+=n){const F=(1-(W-d)/p)*a;F>it&&F<a-it&&u.push(Math.round(W))}return u})():(()=>{const n=p>0?qt(p*O/a):0,u=[];if(n>0){const D=Math.ceil(d/n)*n;for(let W=D;W<=i+n*1e-6;W+=n){const F=(1-(W-d)/p)*a;F>it&&F<a-it&&u.push(W)}}return u})(),A=xe(e.testDefs,e.activeTest),R=e.plotMode==="stackedBins"||e.plotMode==="stackedSoftBins",y=R?"Count":A?.name??(e.activeTest!=null?`Test ${e.activeTest}`:void 0),T=R?void 0:A?.unit,{tickFmt:S}=we(i,y,T,z),P=e.allIntegerValues?n=>String(Math.round(n)):S;t.textBaseline="middle";for(const n of Xt){const u=c+ht(n);t.beginPath(),t.moveTo(f+_,u),t.lineTo(f+_+L,u),t.stroke(),t.fillText(P(n),f+_+L+2,u)}if(t.beginPath(),t.moveTo(f+_,c),t.lineTo(f+_+L,c),t.stroke(),t.textBaseline="top",t.fillText(P(i),f+_+L+2,c),t.beginPath(),t.moveTo(f+_,c+a),t.lineTo(f+_+L,c+a),t.stroke(),t.textBaseline="bottom",t.fillText(P(d),f+_+L+2,c+a),!R&&A&&(A.limitLow!==void 0||A.limitHigh!==void 0)){const n=[],u=e.colorbarRangeMode==="spec";for(const[D,W]of[[A.limitLow,"LSL"],[A.limitHigh,"USL"]]){if(D===void 0||e.logScale&&D<=0)continue;const F=ht(D);!u&&(F<-it||F>a+it)||n.push({value:D,label:W,py:Math.max(0,Math.min(a,F))})}if(n.length>0){t.save(),t.font=v,t.textAlign="right",t.textBaseline="middle";for(const{py:D,label:W}of n){const F=c+D;u||(t.strokeStyle="rgba(255,255,255,0.85)",t.lineWidth=3,t.beginPath(),t.moveTo(f,F),t.lineTo(f+_,F),t.stroke(),t.strokeStyle="rgba(0,0,0,0.7)",t.lineWidth=1,t.beginPath(),t.moveTo(f,F),t.lineTo(f+_,F),t.stroke()),t.fillStyle=M.text,t.fillText(W,f-3,F);const Ht=W==="USL"?-1:1,$t=4,Ne=f-3-t.measureText(W).width-$t-4;t.beginPath(),Ae(t,Ne,F,$t,Ht),t.strokeStyle="#fff",t.lineWidth=2,t.lineJoin="round",t.stroke(),t.fillStyle=M.text,t.fill()}t.restore()}}if(!R){const n=e.logScale?"log\u2081\u2080":e.logScaleRequested?"linear \u2014 log n/a":null;n&&(t.save(),t.fillStyle=M.text,t.font=Ve,t.textAlign="right",t.textBaseline="top",t.fillText(n,I-h,k),t.restore(),k+=15)}Q&&w&&rt(w,I-h,k,"right","top",B(k),Gt,M.text)}const ee=[];let J;if(et){const o=be(e.colorScheme),r=C==="compact",l=e.plotMode==="softBin"?yt:at,a=l?new Map(l.map(s=>[s.bin,s])):null,c=s=>{const x=a?.get(s);return(e.colorScheme==="custom"?x?.color:void 0)??o.forBin(s)},f=(s,x)=>{const w=t.measureText(s).width,k=r?0:t.measureText(String(x)).width,B=X+gt+w+(k?Ut:0);return{labelWidth:w,countWidth:k,totalWidth:B}},d=Mt?.find(s=>s.key===e.activeMetadataKey),i=tt?ot.map(s=>{const x=r?s.compactLabel:s.label;return{key:s.key,color:s.color,count:s.count,label:x,tooltipLabel:s.tooltip,...f(x,s.count)}}):e.plotMode==="metadata"?Rt.map(([s,x])=>{const k=d?.values?.find(q=>q.value===s)?.label??s,B=r?s:k,lt=e.metadataColorMap?.get(s)??"#d6d9dd";return{key:s,color:lt,count:x,label:B,tooltipLabel:`${k} \xB7 ${x} dies`,...f(B,x)}}):At.map(([s,x])=>{const w=a?.get(s),k=w?.name?`${s} \xB7 ${w.name}`:`Bin ${s}`,B=r?String(s):k;return{key:s,color:c(s),count:x,label:B,tooltipLabel:`${k} \xB7 ${x} dies`,...f(B,x)}}),p=I-h*2;if(i.length>0){const s=Math.max(1,Math.floor((H-2*h)/Y));let x=i.length,w=1,k=x*Y,B=[],lt=x,q=0;const ht=zt||_t;if(ht){const A=Math.max(...i.map(y=>y.totalWidth)),R=Math.max(1,Math.min(i.length,Math.floor((p+8)/(A+8))));for(let y=R;y>=1;y--){const T=Math.ceil(i.length/y);if(T>s)continue;const S=new Array(y).fill(0);for(let n=0;n<i.length;n++){const u=n%y;S[u]=Math.max(S[u],i[n].totalWidth)}if(S.reduce((n,u)=>n+u,0)+(y-1)*8<=p||y===1){w=y,B=S,lt=T,x=T,k=T*Y;break}}}else x=Math.min(i.length,s),k=x*Y,i.length>s&&(q=i.length-(s-1)),B=[Wt];let L,O;if(Pt){const A=Math.max(...i.map(u=>X+gt+t.measureText(u.label).width+Ut)),R=Math.ceil(A);B=[R];const y=Q?qe:0,T=R+mt*2,S=k+y+mt*2,P=pt?.x??0,n=pt?.y??0;L=Math.min(Math.max(I-h-T+P,h),I-h-T),O=Math.min(Math.max(H-h-S+n,h),H-h-S),J={x:L,y:O,w:T,h:S},t.save(),t.globalAlpha=.92,t.fillStyle=M.surface,t.fillRect(L,O,T,S),t.globalAlpha=1,t.strokeStyle=M.axisLine,t.lineWidth=1,t.strokeRect(L,O,T,S),t.restore(),L+=mt,O+=mt+y}else zt?(L=h,O=H-h-k):_t?(L=h,O=h+j):ne?(L=h+4,O=h+Math.round((H-2*h-k)/2)):(L=I-h-Wt+4,O=h+Math.round((H-2*h-k)/2));if(Q){const A=e.plotMode==="softBin"?yt:at,{primary:R,secondary:y}=ye(e,z,A),T=6,S=u=>Nt-Math.max(ct(u),ct(u-12))-8,P=u=>dt?I-h:S(u),n=Pt?J.y+J.h:O+k;if(_t){const u=n+T;rt(R,L,u,"left","top",Nt-Math.max(ct(u),ct(u+12))-8),y&&rt(y,L,u+16,"left","top",S(u+16),Gt,M.text)}else if(Pt)rt(R,J.x+mt,J.y+mt,"left","top",J.x+J.w-mt),y&&rt(y,J.x,n+T,"left","top",J.x+J.w,Gt,M.text);else{const u=Math.max(O-T,h+j+12);if(rt(R,L,u,"left","bottom",P(u)),y){const D=n+T;rt(y,L,D,"left","top",P(D),Gt,M.text)}}}t.save(),t.font=v;const it=ht?8:0,Xt=(A,R)=>{if(t.measureText(A).width<=R)return A;let y=0,T=A.length;const S="\u2026",P=t.measureText(S).width;for(;y<T;){const n=y+T+1>>1;t.measureText(A.slice(0,n)).width+P<=R?y=n:T=n-1}return y===0?S:A.slice(0,y)+S};if(ht){let A=0;for(let R=0;R<x;R++){let y=L;for(let T=0;T<w;T++){const S=i[A++];if(!S)break;const P=S.key===E,n=y,u=y+X+gt,D=O+R*Y+Y/2,W=O+R*Y+Math.round((Y-X)/2),F=B[T]-X-gt-Ut;t.fillStyle=S.color,t.fillRect(n,W,X,X),t.strokeStyle=P?M.accent:"rgba(0,0,0,0.25)",t.lineWidth=P?2:.75,t.strokeRect(n,W,X,X),t.fillStyle=P?M.accent:M.text,t.font=P?`bold ${v}`:v,t.textAlign="left",t.textBaseline="middle",t.fillText(Xt(S.label,F),u,D),t.fillStyle=M.textMuted,t.font=v,t.textAlign="right",t.fillText(String(S.count),y+B[T]-2,D),ee.push({bin:S.key,x:y,y:O+R*Y,w:B[T],h:Y,label:S.tooltipLabel}),y+=B[T]+it}}}else{const A=B[0],R=C!=="compact",y=A-X-gt-(R?Ut:0),T=L+A-2;let S=i;q>0&&(S=i.slice(0,s-1));let P=O;for(const n of S){const u=n.key===E,D=L,W=L+X+gt,F=P+Y/2,Ht=P+Math.round((Y-X)/2),$t=n.label;t.fillStyle=n.color,t.fillRect(D,Ht,X,X),t.strokeStyle=u?M.accent:"rgba(0,0,0,0.25)",t.lineWidth=u?2:.75,t.strokeRect(D,Ht,X,X),t.fillStyle=u?M.accent:M.text,t.font=u?`bold ${v}`:v,t.textAlign="left",t.textBaseline="middle",t.fillText(Xt($t,y),W,F),(R||Pt)&&(t.fillStyle=M.textMuted,t.font=v,t.textAlign="right",t.fillText(String(n.count),T,F)),ee.push({bin:n.key,x:L,y:P,w:A,h:Y,label:n.tooltipLabel}),P+=Y}q>0&&(t.fillStyle=M.textMuted,t.font=v,t.textAlign="left",t.textBaseline="middle",t.fillText(`+ ${q} more`,L+X+gt,P+Y/2))}}i.length>0&&t.restore()}const De={originX:nt,originY:st,ppm:b,snapDist:It},ge=e.rectangles[0]?.width??1,me=e.rectangles[0]?.height??1,Ot=ge*1.5,Et=me*1.5,ft=e.dieBounds,Yt=ft?ft.minX:U.length?Math.min(...U.map(o=>o.x)):0,wt=ft?ft.minY:U.length?Math.min(...U.map(o=>o.y)):0,Fe=ft?ft.maxX:U.length?Math.max(...U.map(o=>o.x)):1,Ie=ft?ft.maxY:U.length?Math.max(...U.map(o=>o.y)):1,St=Math.max(1,Math.ceil((Fe-Yt)/Ot)+1),oe=Math.max(1,Math.ceil((Ie-wt)/Et)+1),ue=Array.from({length:St*oe},()=>[]);for(let o=0;o<U.length;o++){const r=Math.floor((U[o].x-Yt)/Ot),l=Math.floor((U[o].y-wt)/Et),a=Math.max(0,Math.min(St-1,r)),c=Math.max(0,Math.min(oe-1,l));ue[c*St+a].push(o)}function pe(o,r,l){const a=Math.max(0,Math.floor((o-l-Yt)/Ot)),c=Math.min(St-1,Math.floor((o+l-Yt)/Ot)),f=Math.max(0,Math.floor((r-l-wt)/Et)),d=Math.min(oe-1,Math.floor((r+l-wt)/Et)),i=[];for(let p=f;p<=d;p++)for(let s=a;s<=c;s++)for(const x of ue[p*St+s])i.push(x);return i}return{hitTarget:{getDieAtPoint(o,r){const l=(o-nt)/b,a=(st-r)/b,c=Math.max(ge,me);for(const i of pe(l,a,c)){const p=e.rectangles[i];if(p&&Math.abs(l-p.x)<=p.width/2&&Math.abs(a-p.y)<=p.height/2)return e.dies[i]??null}let f=null,d=It*It;for(const i of pe(l,a,It)){const p=U[i].x-l,s=U[i].y-a,x=p*p+s*s;x<d&&(d=x,f=e.dies[i]??null)}return f}},viewport:De,binLegendRows:ee,legendBox:J}}function ze(g,e,N,M,h,$,G,_,ut,Z,pt,xt,V){g.save(),g.font=Ue,g.fillStyle=V.text,g.strokeStyle=V.axisLine,g.lineWidth=.5;const K=N-_+4,E=G+ut-4,z=Oe(pt),j=Me(z,1,0),kt=Me(z,0,1),at=1e-9;function yt(m){return Z?Math.abs(m.y)<at&&Math.abs(m.x)>at?{pitch:Z.x,sign:Math.sign(m.x)}:Math.abs(m.x)<at&&Math.abs(m.y)>at?{pitch:Z.y,sign:Math.sign(m.y)}:null:null}const Mt=yt(j),Q=yt(kt);function I(m){return Mt?Math.round(m*Mt.sign/Mt.pitch):m}function H(m){return Q?Math.round(m*Q.sign/Q.pitch):m}function bt(m){if(!m)return qt(50/$);const C=50/(m*$);return Math.max(1,Math.round(C))*m}const Lt=Mt?.pitch,Tt=Q?.pitch,tt=bt(Lt),et=bt(Tt);g.textAlign="center",g.textBaseline="top";const At=Math.ceil((G-M)/$/tt)*tt,Rt=(e-G-M)/$;for(let m=At;m<=Rt;m+=tt){const C=M+m*$;if(C<G||C>e-G)continue;g.beginPath(),g.moveTo(C,K-Re),g.lineTo(C,K),g.stroke();const dt=Z?String(I(m)):Le(m);g.fillText(dt,C,K+2)}g.textAlign="right",g.textBaseline="middle";const ot=Math.ceil((h-(N-G))/$/et)*et,Ct=(h-G)/$;for(let m=ot;m<=Ct;m+=et){const C=h-m*$;if(C<G||C>N-G)continue;g.beginPath(),g.moveTo(E,C),g.lineTo(E+Re,C),g.stroke();const dt=Z?String(H(m)):Le(m);g.fillText(dt,E-2,C)}g.restore()}function qt(g){const e=Math.pow(10,Math.floor(Math.log10(g))),N=g/e;return(N<1.5?1:N<3.5?2:N<7.5?5:10)*e}function Ke(g,e,N,M,h,$,G){const _=[],ut=G;if(ut<1){const V=qt((e-g)*M/N);if(V>0){const K=Math.ceil(g/V)*V;for(let E=K;E<=e+V*1e-6;E+=V){if(E<=g||E>=e)continue;const z=(1-(Math.log10(E)-$)/G)*N;z>h&&z<N-h&&_.push(E)}}return _}const Z=ut<3?[1,2,5]:[1],pt=Math.floor(Math.log10(g)),xt=Math.ceil(Math.log10(e));for(let V=pt;V<=xt;V++)for(const K of Z){const E=K*Math.pow(10,V);if(E<=g||E>=e)continue;const z=(1-(Math.log10(E)-$)/G)*N;z>h&&z<N-h&&_.push(E)}return _}export{fmt,fmtColorbarAxis}from"../renderer/fmt.js";
1
+ import{fontPx as _t}from"./toolbar.js";import{findTestDef as Re,buildMapTitle as _e}from"../renderer/buildView.js";import{affineInvert as Je,affineVector as Ce}from"../core/transforms.js";import{compareNatural as Ze}from"../core/utils.js";import{getColorScheme as Be}from"../renderer/colorSchemes.js";import{SPEC_PASS_FILL as Pe,SPEC_FAIL_LOW as je,SPEC_FAIL_HIGH as We,contrastTextColor as De}from"../renderer/colorMap.js";import{fmt as Oe,fmtColorbarAxis as Qe}from"../renderer/fmt.js";import{resolveCanvasTheme as ve}from"./canvasTheme.js";function Ie(m,e,F,M,h){m.moveTo(e,F+h*M),m.lineTo(e-M*.9,F-h*M*.6),m.lineTo(e+M*.9,F-h*M*.6),m.closePath()}const to=new Set(["value","stackedValues","stackedBins","stackedSoftBins"]),eo=new Set(["hardBin","softBin","metadata"]),Z=()=>`${_t(-2)}px system-ui, sans-serif`,Fe=3,Ut=2,oo=6,no=20,so=()=>`600 ${_t()}px system-ui, sans-serif`,Kt=()=>`${_t(-1)}px system-ui, sans-serif`,lo=()=>`600 ${_t(-1)}px system-ui, sans-serif`,qt=128,io=()=>`${_t(-2)}px system-ui, sans-serif`,Ne=4,D=17,Y=11;export const BIN_LEGEND_W=110,BIN_LEGEND_W_COMPACT=64,BIN_LEGEND_ADAPT_COMPACT=280,BIN_LEGEND_ADAPT_FLOATING=180,BIN_LEGEND_MIN_CANVAS_W=150,BIN_LEGEND_MIN_CANVAS_H=120;const zt=28,mt=5,ut=8,ao=15;export function toCanvas(m,e,F={}){const M=ve(m.parentElement??m),{padding:h=16,showColorbar:H=!0,markFailingDies:X=!1,colorbarWidth:P=16,background:pt=M.background,legendPosition:j="default",legendOffset:xt,showAxes:Mt=!1,diePitchMm:G,viewport:q,activeBin:N,hoverBin:z,fallbackFormat:St,topClearance:Q=0,minRightReserve:ct,hbinDefs:yt,sbinDefs:st,metadataFields:bt,showTitle:rt=!0}=F,O=Math.floor(m.clientWidth||m.width),E=Math.floor(m.clientHeight||m.height),Lt=O>=BIN_LEGEND_MIN_CANVAS_W&&E>=BIN_LEGEND_MIN_CANVAS_H,kt=H&&Lt&&to.has(e.plotMode)&&e.passFailDisplay==="off",ft=H&&Lt&&eo.has(e.plotMode),tt=H&&Lt&&e.plotMode==="value"&&(e.passFailDisplay==="spec"&&e.specCounts!==void 0||e.passFailDisplay==="test"&&e.passFailCounts!==void 0),ht=ft||tt,Ct=ft&&e.plotMode!=="metadata"&&e.binCounts?[...e.binCounts.entries()].sort(([o],[c])=>o-c):[],Bt=ft&&e.plotMode==="metadata"&&e.metadataCounts?[...e.metadataColorMap?.keys()??[...e.metadataCounts.keys()].sort(Ze)].map(o=>[o,e.metadataCounts.get(o)??0]):[],et=[];if(tt&&e.passFailDisplay==="test"){const o=e.passFailCounts;et.push({key:-1,color:Pe,label:"Pass",compactLabel:"Pass",count:o.pass,tooltip:`Pass \xB7 ${o.pass} dies`}),et.push({key:-2,color:We,label:"Fail",compactLabel:"Fail",count:o.fail,tooltip:`Fail \xB7 ${o.fail} dies`})}else if(tt){const o=e.specCounts,c=Re(e.testDefs,e.activeTest);et.push({key:-1,color:Pe,label:"Pass",compactLabel:"Pass",count:o.pass,tooltip:`Pass \xB7 ${o.pass} dies`}),c?.limitHigh!==void 0&&et.push({key:-2,color:We,label:"Fail high",compactLabel:"Fail hi",count:o.failHigh,tooltip:`Fail high \xB7 ${o.failHigh} dies`}),c?.limitLow!==void 0&&et.push({key:-3,color:je,label:"Fail low",compactLabel:"Fail lo",count:o.failLow,tooltip:`Fail low \xB7 ${o.failLow} dies`})}const S=tt?et.length:e.plotMode==="metadata"?Bt.length:Ct.length,L=(m.ownerDocument.defaultView??window).devicePixelRatio??1;if(O<=0||E<=0)return{hitTarget:{getDieAtPoint:()=>null},viewport:{originX:0,originY:0,ppm:1,snapDist:1},binLegendRows:[]};const $=j!=="default"?j:O<BIN_LEGEND_ADAPT_FLOATING?"floating":O<BIN_LEGEND_ADAPT_COMPACT?"compact":"default",re=$==="default"||$==="compact",fe=$==="left",Zt=$==="bottom",Pt=$==="top",Wt=$==="floating",Dt=$==="compact"||$==="floating"?BIN_LEGEND_W_COMPACT:BIN_LEGEND_W,he=Math.floor((E-2*h)/D),de=S>he?he-1:S,ge=ht&&Zt?de*D:0,jt=ht&&Pt?de*D:0,Qt=e.plotMode==="stackedBins"||e.plotMode==="stackedSoftBins",vt=Re(e.testDefs,e.activeTest),Ee=Qt?"Count":vt?.name??(e.activeTest!=null?`Test ${e.activeTest}`:void 0),we=Qt?void 0:vt?.unit,[Ot,It]=e.valueRange,{tickFmt:Ye}=Qe(It,Ee,we,St),Ft=e.allIntegerValues?o=>String(Math.round(o)):Ye;let te=0;if(kt){const o=m.getContext("2d");o.font=Z();const c=[Ft(Ot),Ft(It)];Ot<0&&c.push(Ft(-Math.max(Math.abs(Ot),Math.abs(It))));for(const s of c)te=Math.max(te,o.measureText(s).width)}const me=Math.max(no,Math.ceil(te)+Fe+Ut+oo-h),ee=kt?P+me+8:ht&&re?Dt:0,oe=ht&&fe?Dt:0,ne=Mt?32:0,Nt=Mt?36:0,He=ct!=null?Math.max(ee,ct):ee,Xe=O-2*h-He-Nt-oe,ue=E-2*h-ne-ge-jt;m.width=Math.round(O*L),m.height=Math.round(E*L);const t=m.getContext("2d");t.scale(L,L),t.fillStyle=pt,t.fillRect(0,0,O,E);const V=e.hoverPoints;if(!V.length)return{hitTarget:{getDieAtPoint:()=>null},viewport:{originX:0,originY:0,ppm:1,snapDist:1},binLegendRows:[]};const Et=e.rectangles[0],se=Et?Et.width/2:0,le=Et?Et.height/2:0,wt=e.dieBounds,pe=(wt?.minX??0)-se,$e=(wt?.maxX??0)+se,Ge=(wt?.minY??0)-le,xe=(wt?.maxY??0)+le,Me=$e-pe,ye=xe-Ge;let ot,nt,k;if(q)({originX:ot,originY:nt,ppm:k}=q);else{const c=e.notchDir,s=c&&c.y>.1?13:0,a=c&&c.y<-.1?13:0,r=c&&c.x<-.1?13:0,f=c&&c.x>.1?13:0,d=Xe-r-f,i=ue-Q-s-a;k=Math.min(d/Me,i/ye),ot=h+Nt+oe+r+(d-Me*k)/2-pe*k,nt=h+Q+jt+s+(i-ye*k)/2+xe*k}const Yt=q?.snapDist??Math.max(se,le,1)*1.5,Ht=ot+e.waferCenter.x*k,Ve=nt-e.waferCenter.y*k,ie=e.waferRadius*k,lt=o=>{const c=Math.abs(o-Ve);return c>=ie?0:Math.sqrt(ie*ie-c*c)},it=(o,c,s,a,r,f,d=so(),i=M.text)=>{if(!o)return;t.save(),t.fillStyle=i,t.font=d,t.textAlign=a,t.textBaseline=r;const p=a==="right"?c-f:f-c;let l=o;if(p>8&&t.measureText(l).width>p){for(;l.length>1&&t.measureText(l+"\u2026").width>p;)l=l.slice(0,-1);l=l+"\u2026"}p>8&&t.fillText(l,c,s),t.restore()};if(t.save(),t.setTransform(k*L,0,0,-k*L,ot*L,nt*L),e.rectangles.length>0){const o=new Map;for(const s of e.rectangles){const a=String(s.fill);let r=o.get(a);r||(r=[],o.set(a,r)),r.push(s)}for(const[s,a]of o){t.beginPath();for(const r of a)t.rect(r.x-r.width/2,r.y-r.height/2,r.width,r.height);t.fillStyle=s,t.fill()}t.strokeStyle="rgba(0,0,0,0.18)",t.lineWidth=.5/(k*L),t.beginPath();for(const s of e.rectangles)t.rect(s.x-s.width/2,s.y-s.height/2,s.width,s.height);if(t.stroke(),X){const s=e.rectangles.filter(a=>a.binFail);if(s.length>0){const a=new Map;for(const r of s){const f=De(String(r.fill));let d=a.get(f);d||(d=[],a.set(f,d)),d.push(r)}t.save(),t.lineCap="butt";for(const[r,f]of a){t.strokeStyle=r,t.globalAlpha=.55,t.lineWidth=Math.max(.6/(k*L),Math.min(f[0].width,f[0].height)*.1);for(const d of f){const i=d.width/2,p=d.height/2,l=Math.max(1.6/(k*L),Math.min(d.width,d.height)/3);t.save(),t.beginPath(),t.rect(d.x-i,d.y-p,d.width,d.height),t.clip(),t.beginPath();for(let y=-(i+p);y<=i+p;y+=l)t.moveTo(d.x-i+y,d.y-p),t.lineTo(d.x-i+y+d.height,d.y+p);t.stroke(),t.restore()}}t.restore()}}const c=e.rectangles.filter(s=>s.specMark);if(c.length>0){const s=new Map;for(const r of c){const f=De(String(r.fill)),d=`${r.specMark}|${f}`;let i=s.get(d);i||(i=[],s.set(d,i)),i.push(r)}const a=2.5/(k*L);t.lineJoin="round";for(const[r,f]of s){const[d,i]=r.split("|"),p=i==="#000000"?"#ffffff":"#000000",l=d==="failHigh"?1:-1,y=Math.min(f[0].width,f[0].height),w=Math.max(2.5/(k*L),y*.3);t.beginPath();for(const A of f)Ie(t,A.x,A.y,w,l);t.strokeStyle=p,t.lineWidth=a,t.stroke(),t.fillStyle=i,t.fill()}}}for(const o of e.overlays){t.beginPath();for(const c of o.points){for(let s=0;s<c.length;s++)s===0?t.moveTo(c[s].x,c[s].y):t.lineTo(c[s].x,c[s].y);o.closed&&t.closePath()}o.fill&&!o.fill.startsWith("rgba(0,0,0,0)")&&(t.fillStyle=o.fill,t.fill()),o.kind==="ring-boundary"||o.kind==="quadrant-boundary"||o.kind==="reticle"?(t.strokeStyle="rgba(0,0,0,0.45)",t.lineWidth=3/(k*L),t.stroke(),t.strokeStyle="rgba(255,255,255,0.8)",t.lineWidth=1/(k*L),t.stroke()):(t.strokeStyle=o.lineColor,t.lineWidth=o.lineWidth/(k*L),t.stroke())}if(t.restore(),e.notchDir){const o=ot+e.waferCenter.x*k,c=nt-e.waferCenter.y*k,s=e.notchDir.x,a=-e.notchDir.y,r=e.waferRadius*k,f=4,d=9,i=5,p=o+s*(r+f),l=c+a*(r+f),y=p+s*d,w=l+a*d,A={x:-a,y:s},W=h+Nt+oe,U=O-h-ee,K=h+Q+jt,_=E-h-ge-ne;t.save(),t.beginPath(),t.rect(W,K,U-W,_-K),t.clip(),t.beginPath(),t.moveTo(p,l),t.lineTo(y+A.x*i,w+A.y*i),t.lineTo(y-A.x*i,w-A.y*i),t.closePath(),t.fillStyle=M.text,t.fill(),t.restore()}t.save(),t.textAlign="center",t.textBaseline="middle";let be=0;if(e.rectangles.length>0){let o=1;for(const f of e.texts)f.role!=="indicator"&&f.text.length>o&&(o=f.text.length);const c=e.rectangles[0].width*k,s=e.rectangles[0].height*k,a=Math.min(c,s),r=c/o;be=Math.max(8,Math.min(64,Math.round(Math.min(a*.55,r*1.8))))}for(const o of e.texts){const c=ot+o.x*k,s=nt-o.y*k;t.font=`${o.role==="indicator"?o.fontSize:be}px system-ui, sans-serif`,t.fillStyle=o.color,t.fillText(o.text,c,s)}if(t.restore(),Mt&&co(t,O,E,ot,nt,k,h,ne,Nt,G,e.gridToScreen,e.rotation,M),kt){const o=Be(e.colorScheme),c=me,s=ue-Q,a=Math.round(s*.75),r=h+Q+Math.round((s-a)/2),f=O-h-P-c,d=Ot,i=It,p=i-d,l=e.plotMode==="stackedSoftBins"?st:yt,{primary:y,secondary:w}=rt?_e(e,St,l):{primary:"",secondary:""};if(rt){const n=Math.max(r-6,h+Q+11),b=Ht+Math.max(lt(n),lt(n-12))+8;it(y,O-h,n,"right","bottom",b)}let A=r+a+4;const W=n=>Ht+Math.max(lt(n),lt(n+12))+8,U=e.logScale&&d>0?Math.log10(d):0,K=e.logScale&&i>0?Math.log10(i)-U:1,_=n=>e.logScale?(1-(Math.log10(n)-U)/K)*a:(1-(n-d)/p)*a;for(let n=0;n<qt;n++){const b=1-n/(qt-1),u=r+n/qt*a,g=a/qt+1;t.fillStyle=o.forValue(b),t.fillRect(f,u,P,g)}t.strokeStyle="rgba(0,0,0,0.25)",t.lineWidth=.5,t.strokeRect(f,r,P,a),t.fillStyle=M.text,t.font=Z(),t.textAlign="left",t.strokeStyle="rgba(0,0,0,0.35)",t.lineWidth=.5;const C=Fe,Rt=36,v=14,I=e.logScale&&K>0?ro(d,i,a,Rt,v,U,K):e.allIntegerValues?(()=>{const n=Math.max(1,Jt(p*Rt/a)),b=[],u=Math.ceil(d/n)*n;for(let g=u;g<=i+n*1e-6;g+=n){const B=(1-(g-d)/p)*a;B>v&&B<a-v&&b.push(Math.round(g))}return b})():(()=>{const n=p>0?Jt(p*Rt/a):0,b=[];if(n>0){const u=Math.ceil(d/n)*n;for(let g=u;g<=i+n*1e-6;g+=n){const B=(1-(g-d)/p)*a;B>v&&B<a-v&&b.push(g)}}return b})(),R=vt,x=Qt,T=Ft;t.textBaseline="middle";for(const n of I){const b=r+_(n);t.beginPath(),t.moveTo(f+P,b),t.lineTo(f+P+C,b),t.stroke(),t.fillText(T(n),f+P+C+Ut,b)}if(t.beginPath(),t.moveTo(f+P,r),t.lineTo(f+P+C,r),t.stroke(),t.textBaseline="top",t.fillText(T(i),f+P+C+Ut,r),t.beginPath(),t.moveTo(f+P,r+a),t.lineTo(f+P+C,r+a),t.stroke(),t.textBaseline="bottom",t.fillText(T(d),f+P+C+Ut,r+a),!x&&R&&(R.limitLow!==void 0||R.limitHigh!==void 0)){const n=[],b=e.colorbarRangeMode==="spec";for(const[u,g]of[[R.limitLow,"LSL"],[R.limitHigh,"USL"]]){if(u===void 0||e.logScale&&u<=0)continue;const B=_(u);!b&&(B<-v||B>a+v)||n.push({value:u,label:g,py:Math.max(0,Math.min(a,B))})}if(n.length>0){t.save(),t.font=Z(),t.textAlign="right",t.textBaseline="middle";for(const{py:u,label:g}of n){const B=r+u;b||(t.strokeStyle="rgba(255,255,255,0.85)",t.lineWidth=3,t.beginPath(),t.moveTo(f,B),t.lineTo(f+P,B),t.stroke(),t.strokeStyle="rgba(0,0,0,0.7)",t.lineWidth=1,t.beginPath(),t.moveTo(f,B),t.lineTo(f+P,B),t.stroke()),t.fillStyle=M.text,t.fillText(g,f-3,B);const dt=g==="USL"?-1:1,gt=4,Tt=f-3-t.measureText(g).width-gt-4;t.beginPath(),Ie(t,Tt,B,gt,dt),t.strokeStyle="#fff",t.lineWidth=2,t.lineJoin="round",t.stroke(),t.fillStyle=M.text,t.fill()}t.restore()}}if(!x){const n=e.logScale?"log\u2081\u2080":e.logScaleRequested?"linear \u2014 log n/a":null;n&&(t.save(),t.fillStyle=M.text,t.font=lo(),t.textAlign="right",t.textBaseline="top",t.fillText(n,O-h,A),t.restore(),A+=15)}rt&&w&&it(w,O-h,A,"right","top",W(A),Kt(),M.text)}const ae=[];let J;if(ht){const o=Be(e.colorScheme),c=$==="compact",s=e.plotMode==="softBin"?st:yt,a=s?new Map(s.map(l=>[l.bin,l])):null,r=l=>{const y=a?.get(l);return(e.colorScheme==="custom"?y?.color:void 0)??o.forBin(l)},f=(l,y)=>{const w=t.measureText(l).width,A=c?0:t.measureText(String(y)).width,W=Y+mt+w+(A?zt:0);return{labelWidth:w,countWidth:A,totalWidth:W}},d=bt?.find(l=>l.key===e.activeMetadataKey),i=tt?et.map(l=>{const y=c?l.compactLabel:l.label;return{key:l.key,color:l.color,count:l.count,label:y,tooltipLabel:l.tooltip,...f(y,l.count)}}):e.plotMode==="metadata"?Bt.map(([l,y])=>{const A=d?.values?.find(K=>K.value===l)?.label??l,W=c?l:A,U=e.metadataColorMap?.get(l)??"#d6d9dd";return{key:l,color:U,count:y,label:W,tooltipLabel:`${A} \xB7 ${y} dies`,...f(W,y)}}):Ct.map(([l,y])=>{const w=a?.get(l),A=w?.name?`${l} \xB7 ${w.name}`:`Bin ${l}`,W=c?String(l):A;return{key:l,color:r(l),count:y,label:W,tooltipLabel:`${A} \xB7 ${y} dies`,...f(W,y)}}),p=O-h*2;if(i.length>0){const l=Math.max(1,Math.floor((E-2*h)/D));let y=i.length,w=1,A=y*D,W=[],U=0;const K=Zt||Pt;if(K){const I=Math.max(...i.map(x=>x.totalWidth)),R=Math.max(1,Math.min(i.length,Math.floor((p+8)/(I+8))));for(let x=R;x>=1;x--){const T=Math.ceil(i.length/x);if(T>l)continue;const n=new Array(x).fill(0);for(let u=0;u<i.length;u++){const g=u%x;n[g]=Math.max(n[g],i[u].totalWidth)}if(n.reduce((u,g)=>u+g,0)+(x-1)*8<=p||x===1){w=x,W=n,y=T,A=T*D;break}}}else y=Math.min(i.length,l),A=y*D,i.length>l&&(U=i.length-(l-1)),W=[Dt];let _,C;if(Wt){const I=Math.max(...i.map(g=>Y+mt+t.measureText(g.label).width+zt)),R=Math.ceil(I);W=[R];const x=rt?ao:0,T=R+ut*2,n=A+x+ut*2,b=xt?.x??0,u=xt?.y??0;_=Math.min(Math.max(O-h-T+b,h),O-h-T),C=Math.min(Math.max(E-h-n+u,h),E-h-n),J={x:_,y:C,w:T,h:n},t.save(),t.globalAlpha=.92,t.fillStyle=M.surface,t.fillRect(_,C,T,n),t.globalAlpha=1,t.strokeStyle=M.axisLine,t.lineWidth=1,t.strokeRect(_,C,T,n),t.restore(),_+=ut,C+=ut+x}else Zt?(_=h,C=E-h-A):Pt?(_=h,C=h+Q):fe?(_=h+4,C=h+Math.round((E-2*h-A)/2)):(_=O-h-Dt+4,C=h+Math.round((E-2*h-A)/2));if(rt){const I=e.plotMode==="softBin"?st:yt,{primary:R,secondary:x}=_e(e,St,I),T=6,n=g=>Ht-Math.max(lt(g),lt(g-12))-8,b=g=>re?O-h:n(g),u=Wt?J.y+J.h:C+A;if(Pt){const g=u+T;it(R,_,g,"left","top",Ht-Math.max(lt(g),lt(g+12))-8),x&&it(x,_,g+16,"left","top",n(g+16),Kt(),M.text)}else if(Wt)it(R,J.x+ut,J.y+ut,"left","top",J.x+J.w-ut),x&&it(x,J.x,u+T,"left","top",J.x+J.w,Kt(),M.text);else{const g=Math.max(C-T,h+Q+12);if(it(R,_,g,"left","bottom",b(g)),x){const B=u+T;it(x,_,B,"left","top",b(B),Kt(),M.text)}}}t.save(),t.font=Z();const Rt=K?8:0,v=(I,R)=>{if(t.measureText(I).width<=R)return I;let x=0,T=I.length;const n="\u2026",b=t.measureText(n).width;for(;x<T;){const u=x+T+1>>1;t.measureText(I.slice(0,u)).width+b<=R?x=u:T=u-1}return x===0?n:I.slice(0,x)+n};if(K){let I=0;for(let R=0;R<y;R++){let x=_;for(let T=0;T<w;T++){const n=i[I++];if(!n)break;const b=n.key===N,u=!b&&n.key===z,g=x,B=x+Y+mt,dt=C+R*D+D/2,gt=C+R*D+Math.round((D-Y)/2),Tt=W[T]-Y-mt-zt;u&&(t.fillStyle=M.hoverRow,t.fillRect(x,C+R*D,W[T],D)),t.fillStyle=n.color,t.fillRect(g,gt,Y,Y),t.strokeStyle=b?M.accent:"rgba(0,0,0,0.25)",t.lineWidth=b?2:.75,t.strokeRect(g,gt,Y,Y),t.fillStyle=b?M.accent:M.text,t.font=b?`bold ${Z()}`:Z(),t.textAlign="left",t.textBaseline="middle",t.fillText(v(n.label,Tt),B,dt),t.fillStyle=M.textMuted,t.font=Z(),t.textAlign="right",t.fillText(String(n.count),x+W[T]-2,dt),ae.push({bin:n.key,x,y:C+R*D,w:W[T],h:D,label:n.tooltipLabel}),x+=W[T]+Rt}}}else{const I=W[0],R=$!=="compact",x=I-Y-mt-(R?zt:0),T=_+I-2;let n=i;U>0&&(n=i.slice(0,l-1));let b=C;for(const u of n){const g=u.key===N,B=!g&&u.key===z,dt=_,gt=_+Y+mt,Tt=b+D/2,Ae=b+Math.round((D-Y)/2),ze=u.label;B&&(t.fillStyle=M.hoverRow,t.fillRect(_-3,b,I+3,D)),t.fillStyle=u.color,t.fillRect(dt,Ae,Y,Y),t.strokeStyle=g?M.accent:"rgba(0,0,0,0.25)",t.lineWidth=g?2:.75,t.strokeRect(dt,Ae,Y,Y),t.fillStyle=g?M.accent:M.text,t.font=g?`bold ${Z()}`:Z(),t.textAlign="left",t.textBaseline="middle",t.fillText(v(ze,x),gt,Tt),(R||Wt)&&(t.fillStyle=M.textMuted,t.font=Z(),t.textAlign="right",t.fillText(String(u.count),T,Tt)),ae.push({bin:u.key,x:_,y:b,w:I,h:D,label:u.tooltipLabel}),b+=D}U>0&&(t.fillStyle=M.textMuted,t.font=Z(),t.textAlign="left",t.textBaseline="middle",t.fillText(`+ ${U} more`,_+Y+mt,b+D/2))}}i.length>0&&t.restore()}const Ue={originX:ot,originY:nt,ppm:k,snapDist:Yt},Te=e.rectangles[0]?.width??1,Se=e.rectangles[0]?.height??1,Xt=Te*1.5,$t=Se*1.5,at=e.dieBounds,Gt=at?at.minX:V.length?Math.min(...V.map(o=>o.x)):0,Vt=at?at.minY:V.length?Math.min(...V.map(o=>o.y)):0,Ke=at?at.maxX:V.length?Math.max(...V.map(o=>o.x)):1,qe=at?at.maxY:V.length?Math.max(...V.map(o=>o.y)):1,At=Math.max(1,Math.ceil((Ke-Gt)/Xt)+1),ce=Math.max(1,Math.ceil((qe-Vt)/$t)+1),Le=Array.from({length:At*ce},()=>[]);for(let o=0;o<V.length;o++){const c=Math.floor((V[o].x-Gt)/Xt),s=Math.floor((V[o].y-Vt)/$t),a=Math.max(0,Math.min(At-1,c)),r=Math.max(0,Math.min(ce-1,s));Le[r*At+a].push(o)}function ke(o,c,s){const a=Math.max(0,Math.floor((o-s-Gt)/Xt)),r=Math.min(At-1,Math.floor((o+s-Gt)/Xt)),f=Math.max(0,Math.floor((c-s-Vt)/$t)),d=Math.min(ce-1,Math.floor((c+s-Vt)/$t)),i=[];for(let p=f;p<=d;p++)for(let l=a;l<=r;l++)for(const y of Le[p*At+l])i.push(y);return i}return{hitTarget:{getDieAtPoint(o,c){const s=(o-ot)/k,a=(nt-c)/k,r=Math.max(Te,Se);for(const i of ke(s,a,r)){const p=e.rectangles[i];if(p&&Math.abs(s-p.x)<=p.width/2&&Math.abs(a-p.y)<=p.height/2)return e.dies[i]??null}let f=null,d=Yt*Yt;for(const i of ke(s,a,Yt)){const p=V[i].x-s,l=V[i].y-a,y=p*p+l*l;y<d&&(d=y,f=e.dies[i]??null)}return f}},viewport:Ue,binLegendRows:ae,legendBox:J}}function co(m,e,F,M,h,H,X,P,pt,j,xt,Mt,G){m.save(),m.font=io(),m.fillStyle=G.text,m.strokeStyle=G.axisLine,m.lineWidth=.5;const q=F-P+4,N=X+pt-4,z=Je(xt),St=Ce(z,1,0),Q=Ce(z,0,1),ct=1e-9;function yt(S){return j?Math.abs(S.y)<ct&&Math.abs(S.x)>ct?{pitch:j.x,sign:Math.sign(S.x)}:Math.abs(S.x)<ct&&Math.abs(S.y)>ct?{pitch:j.y,sign:Math.sign(S.y)}:null:null}const st=yt(St),bt=yt(Q);function rt(S){return st?Math.round(S*st.sign/st.pitch):S}function O(S){return bt?Math.round(S*bt.sign/bt.pitch):S}function E(S){if(!S)return Jt(50/H);const L=50/(S*H);return Math.max(1,Math.round(L))*S}const Lt=st?.pitch,kt=bt?.pitch,ft=E(Lt),tt=E(kt);m.textAlign="center",m.textBaseline="top";const ht=Math.ceil((X-M)/H/ft)*ft,Ct=(e-X-M)/H;for(let S=ht;S<=Ct;S+=ft){const L=M+S*H;if(L<X||L>e-X)continue;m.beginPath(),m.moveTo(L,q-Ne),m.lineTo(L,q),m.stroke();const $=j?String(rt(S)):Oe(S);m.fillText($,L,q+2)}m.textAlign="right",m.textBaseline="middle";const Bt=Math.ceil((h-(F-X))/H/tt)*tt,et=(h-X)/H;for(let S=Bt;S<=et;S+=tt){const L=h-S*H;if(L<X||L>F-X)continue;m.beginPath(),m.moveTo(N,L),m.lineTo(N+Ne,L),m.stroke();const $=j?String(O(S)):Oe(S);m.fillText($,N-2,L)}m.restore()}function Jt(m){const e=Math.pow(10,Math.floor(Math.log10(m))),F=m/e;return(F<1.5?1:F<3.5?2:F<7.5?5:10)*e}function ro(m,e,F,M,h,H,X){const P=[],pt=X;if(pt<1){const G=Jt((e-m)*M/F);if(G>0){const q=Math.ceil(m/G)*G;for(let N=q;N<=e+G*1e-6;N+=G){if(N<=m||N>=e)continue;const z=(1-(Math.log10(N)-H)/X)*F;z>h&&z<F-h&&P.push(N)}}return P}const j=pt<3?[1,2,5]:[1],xt=Math.floor(Math.log10(m)),Mt=Math.ceil(Math.log10(e));for(let G=xt;G<=Mt;G++)for(const q of j){const N=q*Math.pow(10,G);if(N<=m||N>=e)continue;const z=(1-(Math.log10(N)-H)/X)*F;z>h&&z<F-h&&P.push(N)}return P}export{fmt,fmtColorbarAxis}from"../renderer/fmt.js";
@@ -3,8 +3,172 @@ import type { Die } from '../core/dies.js';
3
3
  import type { TestDef, MetadataFieldDef } from '../renderer/buildWaferMap.js';
4
4
  import type { StatsSeverity } from '../stats/types.js';
5
5
  export declare function logWmapVersionOnce(): void;
6
- export declare const WMAP_TOKEN_NAMES: readonly ["z", "icon", "icon-hover", "icon-active", "bg-hover", "bg-active", "separator", "surface", "border", "menu-hover", "menu-active", "panel-bg", "text-muted", "text", "text-strong", "warn-bg", "warn-border", "warn-text", "err-bg", "err-border", "err-text", "info-bg", "info-text", "selected", "finding-indicator", "canvas-bg", "bar-fill", "bar-fill-muted"];
6
+ export declare const WMAP_TOKEN_NAMES: readonly ["z", "icon", "icon-hover", "icon-active", "bg-hover", "bg-active", "separator", "surface", "border", "control-border", "menu-hover", "menu-active", "font-family", "font-size", "density", "panel-bg", "text-muted", "text", "text-strong", "warn-bg", "warn-border", "warn-text", "err-bg", "err-border", "err-text", "info-bg", "info-text", "selected", "finding-indicator", "canvas-bg", "bar-fill"];
7
7
  export type WmapTokenName = (typeof WMAP_TOKEN_NAMES)[number];
8
+ /**
9
+ * The shapes an interactive element comes in. Nineteen sites set
10
+ * `cursor: pointer` with their own padding, border, radius and hover handling;
11
+ * they were not nineteen different controls, but four shapes restated by hand.
12
+ * That restatement is the mechanism behind most of the drift this file's other
13
+ * scales exist to stop.
14
+ *
15
+ * These return STYLE, not elements — call sites keep their own markup and event
16
+ * wiring, and merely stop deciding what a button looks like. Spread first, then
17
+ * add layout:
18
+ *
19
+ * Object.assign(btn.style, { ...controlStyle('outlined'), marginLeft: 'auto' });
20
+ */
21
+ export type ControlKind =
22
+ /** Bordered, transparent ground — the default button (Summary report, Export CSV). */
23
+ 'outlined'
24
+ /** No border or ground — icon buttons and inline text actions. */
25
+ | 'bare'
26
+ /** Bordered, tinted when on — segmented options and filter toggles. */
27
+ | 'toggle';
28
+ /**
29
+ * Give a control the library's hover feedback. `controlStyle` returns static
30
+ * style and cannot attach listeners, so this is the second half of the pair —
31
+ * call it wherever you spread `controlStyle`.
32
+ *
33
+ * Without it a bordered control is indistinguishable from a bordered label: it
34
+ * has the shape of a button and does nothing when the pointer is over it.
35
+ * wmap's toolbar icon buttons wired this from the start; its outlined TEXT
36
+ * buttons (Summary report, View die list, the CSV exports, Back) wired nothing
37
+ * at all, so the two halves of the same library disagreed about whether a
38
+ * button reacts. tsmap's `.tb-btn` has always had it.
39
+ */
40
+ export declare function wireControlHover(el: HTMLElement, kind?: ControlKind): void;
41
+ export declare function controlStyle(kind: ControlKind, on?: boolean): Partial<CSSStyleDeclaration>;
42
+ export declare const SPACE: {
43
+ xxs: string;
44
+ xs: string;
45
+ sm: string;
46
+ md: string;
47
+ lg: string;
48
+ xl: string;
49
+ xxl: string;
50
+ xxxl: string;
51
+ };
52
+ /**
53
+ * Elevation. Fourteen distinct box-shadow literals existed across the two repos
54
+ * with no rule for which belonged where, so every new component invented one.
55
+ * Three tiers is what the values actually clustered into.
56
+ */
57
+ export declare const SHADOW: {
58
+ /** Resting surface — a panel or card sitting on the page. */
59
+ panel: string;
60
+ /** Transient overlay — menu, dropdown, popover, tooltip. */
61
+ menu: string;
62
+ /** Modal — the only thing above everything else. */
63
+ modal: string;
64
+ };
65
+ /**
66
+ * Motion. One duration and one easing: a UI this dense has no reason to vary
67
+ * them, and ten different `transition` literals is not expressiveness, it is
68
+ * ten authors guessing. Respect `prefers-reduced-motion` at the call site.
69
+ */
70
+ export declare const MOTION: {
71
+ /** State changes — hover, active, disclosure. */
72
+ fast: string;
73
+ /** Movement the eye should follow — a chevron rotating, a panel sliding. */
74
+ base: string;
75
+ };
76
+ /** Line height. Tight for single-line chrome, base for prose. */
77
+ export declare const LEADING: {
78
+ none: string;
79
+ tight: string;
80
+ base: string;
81
+ };
82
+ /** Opacity. `muted` de-emphasises; `disabled` is the one WCAG-relevant value. */
83
+ export declare const ALPHA: {
84
+ muted: string;
85
+ disabled: string;
86
+ };
87
+ /** Uppercase micro-label tracking — one value, not four. */
88
+ export declare const TRACKING = "0.05em";
89
+ /**
90
+ * Horizontal gutter between a scrolling surface's cards and the edge of the
91
+ * window (or host container) they scroll inside.
92
+ *
93
+ * A card is a bounded surface: flush to the viewport you see three borders and
94
+ * the screen edge standing in for the fourth, which reads as clipped rather
95
+ * than as a design choice. This is the gap that stops that.
96
+ *
97
+ * Why 12 (`SPACE.xl`): three different insets were in use when this was added —
98
+ * the Insights bands at 10, the gallery bar at 4, and tsmap's own `#toolbar` at
99
+ * 12 — so there was no incumbent value to inherit. 12 is on the `SPACE` scale,
100
+ * sits at the tight end of the 12-24px page-margin range the major design
101
+ * systems use (Material/HIG 16, Bootstrap 12), which is the right end for a
102
+ * dense analysis tool, and is the only candidate with an anchor outside this
103
+ * library: it matches the host toolbar directly above the map, so cards line up
104
+ * with the toolbar's own content rather than missing it by a couple of pixels.
105
+ *
106
+ * Deliberately NOT applied to the wafer map canvas, which stays full-bleed —
107
+ * map area is the priority and a margin would only shrink it. Same exception
108
+ * UI_STANDARDS.md already names for content that wants to fill its box.
109
+ */
110
+ export declare const EDGE_GUTTER: string;
111
+ /**
112
+ * Inset for a wafer map's OWN chrome — its chrome row (identity + toolbar) and
113
+ * a Summary panel docked inside the map area — from the edge of that map area.
114
+ *
115
+ * Deliberately small, and deliberately NOT `EDGE_GUTTER`. That one is the gap
116
+ * between a bounded surface and the edge of the REGION it lives in. A map
117
+ * embedded in a gallery card is already inside such a region: the card supplies
118
+ * the outer inset, and a second 12px within it stacks two gutters — the thing
119
+ * UI_STANDARDS.md's gutter rule forbids. It reads exactly like what it is, a
120
+ * toolbar standing 12px off the side of a card.
121
+ *
122
+ * A STANDALONE map is the other case: there the map area IS the region, so its
123
+ * chrome takes `EDGE_GUTTER` like any other bounded surface against an edge.
124
+ * `RenderOptions.chromeInset` is how the caller says which it is —
125
+ * `renderWaferGallery` passes this value for its cards, everything else takes
126
+ * the `EDGE_GUTTER` default.
127
+ *
128
+ * This constant was deleted once, when the toolbar moved out of the map box and
129
+ * the Summary panel was briefly its only user. That was wrong: the chrome row
130
+ * inherited the same context problem the moment it started carrying the
131
+ * toolbar, and giving every map `EDGE_GUTTER` put the two-gutter bug straight
132
+ * back into gallery cards.
133
+ */
134
+ export declare const MAP_CHROME_INSET: string;
135
+ export declare const RADIUS: {
136
+ /** Buttons, inputs, menu rows, segmented toggles, swatches. */
137
+ control: string;
138
+ /** Cards, panels, menus, modals — anything that contains other components. */
139
+ container: string;
140
+ /** Badges, count pills, status chips — fully rounded, size-independent. */
141
+ pill: string;
142
+ };
143
+ export declare const FONT: {
144
+ /**
145
+ * The typeface. wmap is embedded in a host and should read as part of it, so
146
+ * this INHERITS by default rather than imposing a stack — a host that sets
147
+ * nothing still gets the fallback below.
148
+ *
149
+ * There were three different hardcoded stacks across five sites
150
+ * (`system-ui, sans-serif` in the summary panel and toolbar,
151
+ * `system-ui, -apple-system, "Segoe UI", sans-serif` in the gallery), none
152
+ * matching tsmap's own `-apple-system`-first body stack. On Linux those
153
+ * resolve to different faces, so the summary panel's value table and the
154
+ * Insights value table — the same data, side by side — rendered in different
155
+ * typefaces.
156
+ */
157
+ family: string;
158
+ meta: string;
159
+ body: string;
160
+ sub: string;
161
+ heading: string;
162
+ stat: string;
163
+ };
164
+ /**
165
+ * The resolved numeric size for canvas `ctx.font`, in px. `delta` matches FONT's
166
+ * tiers for 0 and above (`body`, `sub`, `heading`, `stat`) and `-1` for `meta`.
167
+ * `-2` has no DOM tier by design: it is the map canvas's plot-coupled floor
168
+ * (colorbar tick labels, axis ticks — see toCanvas.ts), which annotates dense
169
+ * data rather than chrome and so sits below the 11px DOM minimum.
170
+ */
171
+ export declare function fontPx(delta?: number, el?: Element | null): number;
8
172
  export declare const CLR: {
9
173
  icon: string;
10
174
  iconHover: string;
@@ -14,8 +178,10 @@ export declare const CLR: {
14
178
  separator: string;
15
179
  menuBg: string;
16
180
  menuBorder: string;
181
+ controlBorder: string;
17
182
  menuHover: string;
18
183
  menuActive: string;
184
+ canvasBg: string;
19
185
  panelBg: string;
20
186
  label: string;
21
187
  value: string;
@@ -31,7 +197,6 @@ export declare const CLR: {
31
197
  findingHighlight: string;
32
198
  findingIndicator: string;
33
199
  barFill: string;
34
- barFillMuted: string;
35
200
  };
36
201
  /**
37
202
  * Findings-severity → colour, shared by summaryPanel.ts (severity dots/borders
@@ -110,7 +275,29 @@ export declare function setDetachWindowOpener(opener: DetachWindowOpener | null)
110
275
  * window is treated as titleless regardless of `document.title`). Navigating
111
276
  * to a real URL would fix it but means either re-running the host page's own
112
277
  * script inside the popup (real risk of double side effects) or requiring a
113
- * dedicated blank same-origin asset — not worth it for a cosmetic issue. */
278
+ * dedicated blank same-origin asset — not worth it for a cosmetic issue.
279
+ *
280
+ * Why this looks different from renderWaferMap's own "Expand" (a modal with
281
+ * wmap's own JS-drawn title bar, see `openOverlay` below): a real
282
+ * `window.open()` popup always carries genuine OS/browser chrome (title bar,
283
+ * and — deliberately, for anti-phishing reasons — an address-bar-ish
284
+ * element), and no window-features flag suppresses that in modern browsers;
285
+ * the feature string above (`width=560,height=600`) is sized-only for
286
+ * exactly this reason. This is a real inconsistency (raised 2026-08-29):
287
+ * `openWindowForCard`'s own fallback path (`window.open` returning null,
288
+ * e.g. Tauri's WebView) already routes through `openFloatingWindow` —
289
+ * wmap's own chrome, pixel-identical to the modal — so the mismatch is
290
+ * scoped to the real-popup case specifically.
291
+ *
292
+ * Possible future change: make `openWindowForCard` always take the
293
+ * `openFloatingWindow` branch (never call this function), for visual
294
+ * consistency with the standalone map's modal everywhere. Deliberately not
295
+ * done: the whole reason gallery cards detach into real windows rather than
296
+ * modals is so several can be open at once for side-by-side comparison,
297
+ * including dragged out to a second monitor — `openFloatingWindow`'s content
298
+ * is confined inside the host page/tab, so forcing it would trade away that
299
+ * capability for cosmetic consistency. Revisit only if that comparison
300
+ * workflow turns out not to matter in practice. */
114
301
  export declare function openDetachWindow(label: string): Window | null;
115
302
  export declare const MODE_LABELS: Record<PlotMode, string>;
116
303
  export declare const BIN_LEGEND_MODES: Set<PlotMode>;
@@ -170,6 +357,29 @@ export declare function saveTextFile(text: string, filename: string, mimeType: s
170
357
  * window." Passing the popup's own `window` fixes this.
171
358
  */
172
359
  export declare function nextFrame(fn: () => void, ownerWindow?: Window): void;
360
+ /**
361
+ * Wire roving-focus keyboard navigation onto an already-roled menu:
362
+ * ArrowUp/Down move between items, Home/End jump to ends, Enter/Space activate
363
+ * the focused item, Escape (or Tab) closes the menu and returns focus to the
364
+ * trigger. `close` should remove the menu and clear any open-menu bookkeeping.
365
+ * Focus moves to the first item on open. Returns nothing; the listener lives on
366
+ * the menu element and dies with it.
367
+ */
368
+ /**
369
+ * Roving keyboard focus over a live list of options — Arrow/Home/End to move,
370
+ * Enter/Space to activate, Escape/Tab to dismiss.
371
+ *
372
+ * Deliberately does NOT move focus itself: that is the one thing the two
373
+ * callers genuinely disagree about. A toolbar menu wants its first item focused
374
+ * on open; the searchable list in `chartShell.ts` must leave focus on its
375
+ * search box, and had its own copy of this whole switch for that single
376
+ * difference — thirty duplicated lines guarding one line of divergence.
377
+ *
378
+ * `items()` is called per keystroke, not captured: the searchable list filters
379
+ * its rows as you type, and a snapshot would navigate rows that are no longer
380
+ * shown.
381
+ */
382
+ export declare function wireListNavigation(container: HTMLElement, items: () => HTMLElement[], dismiss: () => void): void;
173
383
  export declare function overlayRootFor(anchor: Element): Element;
174
384
  export declare function menuLayerFor(anchor: Element): HTMLElement;
175
385
  /** The shared tooltip element for `doc` (default: the host page's own
@@ -202,7 +412,48 @@ export declare function reparentTooltip(parent?: HTMLElement): void;
202
412
  * map), and the `parentElement !==` guard means the DOM is only actually
203
413
  * touched when the root has changed, not on every mousemove.
204
414
  */
415
+ /**
416
+ * Place the tooltip near the cursor without covering the thing it describes.
417
+ *
418
+ * `anchor` used to be consulted only for `overlayRootFor` — placement was
419
+ * computed from the cursor point alone, and since `y` starts ABOVE the cursor
420
+ * (`clientY - 8`) the box then extended down over whatever was being pointed at.
421
+ * On the map canvas that is harmless and in fact necessary: the anchor is the
422
+ * whole canvas, so "don't overlap the anchor" would push the tooltip off the map
423
+ * entirely. On a small anchor — a bar, a badge, a row, an icon button — there IS
424
+ * somewhere else to go, and covering it while describing it is a real defect.
425
+ *
426
+ * So the anchor's box is avoided only when the anchor is small enough for that to
427
+ * be feasible: flip to just below it, else just above it. A large anchor keeps
428
+ * exactly the previous cursor-following behaviour.
429
+ */
205
430
  export declare function positionTooltip(tooltip: HTMLDivElement, anchor: Element, clientX: number, clientY: number): void;
431
+ /**
432
+ * Wire the shared themed tooltip onto a plain DOM element.
433
+ *
434
+ * Hoisted out of `maplessSummary.ts`, which had the only copy — and whose comment
435
+ * already recorded why this exists: a native `title` is a different, browser-native
436
+ * tooltip, slow to appear (OS hover delay) and in an unthemed system font/colour,
437
+ * while every hover surface built through `createToolbarHelpers` uses this instant
438
+ * dark one. `summaryPanel.ts` and the hand-built expand buttons in
439
+ * `renderWaferMap`/`renderWaferGallery` were still on `title`.
440
+ *
441
+ * `text` may be a string, a getter (for a hint that changes with the control's own
442
+ * state, e.g. the findings severity chips' show/hide), or omitted to live-read the
443
+ * element's `aria-label` at hover time — the same thing `makeBtn` does, so a
444
+ * control whose label changes with its state (expand ⇄ reattach) stays correct
445
+ * without re-wiring.
446
+ *
447
+ * `asDataPoint` is for an element that is NOT already a control: it makes the
448
+ * element a tab stop, announces it as a single datum (`role="img"`), and sets
449
+ * `aria-label`, so the tooltip's content reaches the keyboard and a screen reader
450
+ * (WCAG 1.4.13 — content shown on hover must be reachable and dismissable without
451
+ * a pointer). Never pass it for a `<button>`: that would add a nested focus stop
452
+ * and overwrite the control's own accessible name.
453
+ */
454
+ export declare function wireTooltip(target: HTMLElement, text?: string | (() => string), opts?: {
455
+ asDataPoint?: boolean;
456
+ }): void;
206
457
  export interface ToolbarHelpers {
207
458
  makeBtn(iconKey: string, label: string, onClick: () => void): HTMLButtonElement;
208
459
  setActive(btn: HTMLButtonElement, active: boolean): void;
@@ -302,8 +553,9 @@ export interface ExpandToggleHandle {
302
553
  /**
303
554
  * Shared "click/Enter/Space toggles an expand/collapse state, Escape or an
304
555
  * outside click closes it" wiring — the small interaction pattern behind both
305
- * the metadata badge's own expand (metadataBadge.ts) and the gallery card
306
- * header's metadata reveal (renderWaferGallery.ts). `trigger` gets
556
+ * the shared identity header's metadata reveal (identityHeader.ts, used by
557
+ * renderWaferMap.ts) and the gallery card header's own reveal
558
+ * (renderWaferGallery.ts). `trigger` gets
307
559
  * `role="button"`/`tabindex` and the event listeners; `onChange(open)` is
308
560
  * called whenever the state actually changes (never redundantly) — the
309
561
  * caller owns all the resulting DOM (aria-expanded text, panel visibility,
@@ -334,13 +586,27 @@ export type CheckMenuRow = {
334
586
  * Each row with an `onClick` is a toggleable item; rows with `section` are headers.
335
587
  * Stays open after each click so the user can toggle multiple items.
336
588
  */
337
- export declare function buildCheckMenuEl(anchorRect: DOMRect, rows: CheckMenuRow[], helpers: Pick<ToolbarHelpers, 'makeMenuRow' | 'makeMenuSection'>, ownerWindow?: Window): HTMLDivElement;
589
+ export declare function buildCheckMenuEl(anchorRect: DOMRect, rows: CheckMenuRow[], helpers: Pick<ToolbarHelpers, 'makeMenuSection'>, ownerWindow?: Window): HTMLDivElement;
338
590
  export declare function createToolbarHelpers(tooltip: HTMLDivElement): ToolbarHelpers;
339
591
  export type OverlayMode = 'modal' | 'window';
340
592
  export interface OverlayOptions {
341
593
  /** Optional title shown in the header left. */
342
594
  title?: string;
343
595
  mode: OverlayMode;
596
+ /**
597
+ * Override the box's natural (non-maximized) size. Defaults to a 700px
598
+ * square, which suits a circular wafer map — the content this overlay was
599
+ * built for — but is actively wrong for wide content: the Insights suite
600
+ * lays out ~1330px wide in a normal page, so a 700px square modal makes
601
+ * "expand" produce something SMALLER than the view it expanded from.
602
+ *
603
+ * Also the restore target for maximize and minimize, so an overridden size
604
+ * survives both round trips.
605
+ */
606
+ boxSize?: {
607
+ width: string;
608
+ height: string;
609
+ };
344
610
  /**
345
611
  * Called when the maximized state changes — use to reparent tooltips etc. Uses
346
612
  * a CSS maximize (the box grows to fill its backdrop/the viewport), not the
@@ -412,6 +678,11 @@ export declare function openFloatingWindow(opts: Omit<OverlayOptions, 'mode'>):
412
678
  export interface ReparentModalOptions {
413
679
  ownerDocument?: Document;
414
680
  title?: string;
681
+ /** Natural box size — see `OverlayOptions.boxSize`. */
682
+ boxSize?: {
683
+ width: string;
684
+ height: string;
685
+ };
415
686
  /** Called after the modal has closed and every element has been restored to its original position (or appended back, if its original spot is no longer available — see the stale-reference note above). Runs once, even if nothing was actually moved (the re-entrancy guard tripped). */
416
687
  onClosed?: () => void;
417
688
  }
@@ -460,6 +731,39 @@ export declare function requestedPassFailDisplay(opts: {
460
731
  * test returns no entries at all — its value mode IS test pass/fail, there is
461
732
  * no alternative display to choose.
462
733
  */
734
+ /** The overlay flags an overlays menu offers, however the host stores them. */
735
+ export interface OverlayFlags {
736
+ showRingBoundaries?: boolean;
737
+ showQuadrantBoundaries?: boolean;
738
+ showDieLabels?: boolean;
739
+ showReticle?: boolean;
740
+ showXYIndicator?: boolean;
741
+ passFailDisplay?: 'off' | 'spec' | 'test';
742
+ }
743
+ /**
744
+ * The Overlays menu's rows — the five geometry toggles plus whatever
745
+ * `passFailMenuRows` offers for the active test.
746
+ *
747
+ * Built identically by `renderWaferMap` and `renderWaferGallery`, which is the
748
+ * problem it solves: the two lists were the same five labels in the same order
749
+ * with the same option keys, differing only in where the flags live
750
+ * (`viewOpts` vs `sharedOpts`) and how a change is applied (`applyOpts` vs
751
+ * `updateShared`). Adding a sixth overlay meant remembering both, and the map
752
+ * and the gallery quietly offering different menus is the failure that follows
753
+ * from forgetting.
754
+ *
755
+ * `reticleEnabled` stays a caller input: a single map knows whether ITS wafer
756
+ * has a reticle, and the gallery whether ANY item does — genuinely different
757
+ * questions with the same answer type.
758
+ */
759
+ export declare function overlayMenuRows(flags: OverlayFlags, reticleEnabled: boolean, passFail: {
760
+ functionalActive: boolean;
761
+ hasLimits: boolean;
762
+ hasRecorded: boolean;
763
+ }, apply: (patch: OverlayFlags) => void): CheckMenuRow[];
764
+ /** Whether any overlay is on — the Overlays button's "active" state. Shared for
765
+ * the same reason as the rows: it enumerates the same five flags. */
766
+ export declare function anyOverlayActive(flags: OverlayFlags): boolean;
463
767
  export declare function passFailMenuRows(state: {
464
768
  functionalActive: boolean;
465
769
  hasLimits: boolean;