@wafertools/wafermap 0.29.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/AGENTS.md +26 -17
  2. package/CHANGELOG.md +410 -0
  3. package/README.md +6 -6
  4. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  9. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +1 -0
  10. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  13. package/dist/packages/canvas-adapter/deprecated.d.ts +7 -0
  14. package/dist/packages/canvas-adapter/deprecated.js +1 -0
  15. package/dist/packages/canvas-adapter/dieList.d.ts +1 -3
  16. package/dist/packages/canvas-adapter/dieList.js +7 -7
  17. package/dist/packages/canvas-adapter/exportName.d.ts +68 -0
  18. package/dist/packages/canvas-adapter/exportName.js +1 -0
  19. package/dist/packages/canvas-adapter/icons.js +2 -2
  20. package/dist/packages/canvas-adapter/index.d.ts +2 -3
  21. package/dist/packages/canvas-adapter/index.js +1 -1
  22. package/dist/packages/canvas-adapter/insightsTab.d.ts +0 -1
  23. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  24. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  25. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +23 -40
  26. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  27. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +96 -104
  28. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  29. package/dist/packages/canvas-adapter/summaryPanel.d.ts +31 -13
  30. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  31. package/dist/packages/canvas-adapter/toCanvas.d.ts +20 -12
  32. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  33. package/dist/packages/canvas-adapter/toolbar.d.ts +6 -0
  34. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  35. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  36. package/dist/packages/canvas-adapter/userGuideHtml.js +26 -15
  37. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  38. package/dist/packages/canvas-adapter/version.js +1 -1
  39. package/dist/packages/canvas-adapter/warnings.js +1 -1
  40. package/dist/packages/core/deprecated.d.ts +60 -0
  41. package/dist/packages/core/deprecated.js +1 -0
  42. package/dist/packages/core/index.d.ts +1 -3
  43. package/dist/packages/core/index.js +1 -1
  44. package/dist/packages/core/passBins.d.ts +33 -0
  45. package/dist/packages/core/passBins.js +1 -0
  46. package/dist/packages/core/utils.d.ts +12 -0
  47. package/dist/packages/core/utils.js +1 -1
  48. package/dist/packages/core/waferLabel.d.ts +35 -0
  49. package/dist/packages/core/waferLabel.js +1 -0
  50. package/dist/packages/renderer/binColors.d.ts +105 -13
  51. package/dist/packages/renderer/binColors.js +1 -1
  52. package/dist/packages/renderer/buildView.d.ts +8 -9
  53. package/dist/packages/renderer/buildView.js +1 -1
  54. package/dist/packages/renderer/buildWaferMap.d.ts +76 -17
  55. package/dist/packages/renderer/buildWaferMap.js +1 -1
  56. package/dist/packages/renderer/colorMap.d.ts +4 -3
  57. package/dist/packages/renderer/colorSchemes.d.ts +8 -6
  58. package/dist/packages/renderer/deprecate.d.ts +31 -0
  59. package/dist/packages/renderer/deprecate.js +1 -0
  60. package/dist/packages/renderer/deprecated.d.ts +45 -0
  61. package/dist/packages/renderer/deprecated.js +1 -0
  62. package/dist/packages/renderer/index.d.ts +4 -4
  63. package/dist/packages/renderer/index.js +1 -1
  64. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  65. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  66. package/dist/packages/stats/binPareto.d.ts +6 -1
  67. package/dist/packages/stats/capability.d.ts +16 -1
  68. package/dist/packages/stats/capability.js +1 -1
  69. package/dist/packages/stats/clusterDetection.d.ts +0 -2
  70. package/dist/packages/stats/deprecated.d.ts +82 -0
  71. package/dist/packages/stats/deprecated.js +1 -0
  72. package/dist/packages/stats/index.d.ts +5 -14
  73. package/dist/packages/stats/index.js +1 -1
  74. package/dist/packages/stats/normalizeInput.d.ts +10 -0
  75. package/dist/packages/stats/normalizeInput.js +1 -0
  76. package/dist/packages/stats/regions.d.ts +9 -2
  77. package/dist/packages/stats/regions.js +1 -1
  78. package/dist/packages/stats/renderFindingsReport.js +1 -1
  79. package/dist/packages/stats/renderSummaryReport.d.ts +42 -1
  80. package/dist/packages/stats/renderSummaryReport.js +34 -34
  81. package/dist/packages/stats/reportHtml.d.ts +0 -1
  82. package/dist/packages/stats/reportHtml.js +10 -10
  83. package/dist/packages/stats/summaryFigures.d.ts +47 -0
  84. package/dist/packages/stats/summaryFigures.js +1 -0
  85. package/dist/packages/stats/types.d.ts +118 -43
  86. package/dist/packages/stats/yield.d.ts +12 -6
  87. package/dist/packages/stats/yield.js +1 -1
  88. package/package.json +1 -1
@@ -1 +1 @@
1
- import{buildTestBoxplotData as Je}from"../../stats/boxplot.js";import{SPACE as Be,fontPx as X,FONT as Ze,CLR as et}from"../toolbar.js";import{fmt as de}from"../../renderer/fmt.js";import{QUANTITY as De}from"./palette.js";import{cardShell as tt,observeResize as nt,makeTooltip as ot,positionChartTooltip as it,makeBackButton as lt,makeLinkedTestSelect as st,makeToggle as at,makeLinkedAxisPrefs as rt,renderEmptyState as Ie,growCardToFitContent as ct,resolveChartCanvasColors as ft,makeAxisFormat as ut,resolveAxisRange as mt,shouldIncludeLimitsByDefault as dt,drawOffAxisLimits as xt,limitLabelSide as pt,PADDING as L,VALUE_WIDTH as ht,prepareCanvas as gt}from"./chartShell.js";const M=24,k=5,Re=110,bt=12,He=20;export function renderBoxplotPanel(h){const{title:J="Test value distribution",items:Oe,testDefs:xe,onSaveImage:We,groups:m,groupLabelText:Z="group",onOpen:N}=h,Ee=h.openActionLabel??"open that wafer",Pe=h.openActionLabel??"open this wafer",{card:a,heading:_e,body:d,controlsRow:q}=tt(J,We,h.ownerDocument);a.style.flex="0 0 auto",d.style.flex="0 0 auto",a.style.alignSelf="start";const ee=xe.filter(n=>n.testNumber!==void 0);let v=h.selectedTestNumber??ee[0]?.testNumber??null,te=!1,Y=0,x=null,I=null,ne=null,pe=null;function Ge(){const n=!m||m.length===0?"":x??"\0overview";if(ne&&pe===n)return ne;const u=!m||m.length===0?Oe:x!==null?m.find(g=>g.key===x)?.items??[]:m.map(g=>({label:g.key,dies:g.items.flatMap(c=>c.dies??[])}));return ne=u,pe=n,u}function he(){x!==null&&!I?(I=lt(()=>{ge(null),h.onGroupChange?.(null)},a.ownerDocument),q.appendChild(I)):x===null&&I&&(I.remove(),I=null),_e.textContent=x!==null?`${J} \u2014 ${Z}: ${x}`:J}function ge(n){return x===n||n!==null&&!(m??[]).some(u=>u.key===n)?!1:(x=n,he(),Q(),C(),!0)}const be=st(ee,v,n=>{v=n,C(),h.onTestChange?.(n)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Xe=be.el;q.appendChild(Xe),q.appendChild(at("Log scale",te,n=>{te=n,C()},a.ownerDocument));const oe=a.ownerDocument.createElement("span");Object.assign(oe.style,{display:"inline-flex",gap:Be.lg,alignItems:"center"}),q.appendChild(oe);const ie=rt(oe,h.axisPrefs,n=>{h.onAxisPrefsChange?.(n),C()},a.ownerDocument),le=a.ownerDocument.createElement("div");Object.assign(le.style,{color:et.label,fontSize:Ze.body,marginBottom:Be.sm}),a.insertBefore(le,d);function Q(){const n=!!m&&m.length>0&&x===null,u=[];n?u.push(`click a ${Z}'s box to see it by wafer`):N&&u.push(`click a box to ${Ee}`);const g=u.length?`${u[0][0].toUpperCase()}${u[0].slice(1)} \xB7 `:"";le.textContent=`${g}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(Y?` \xB7 axis clipped, ${Y} value${Y===1?"":"s"} outside`:"")}Q(),he();const H=ot(a);let se=null;function C(){const{includeLimits:n,clipOutliers:u}=ie.get();if(Q(),d.innerHTML="",ee.length===0||v===null){Ie(d,"No parametric test data available for box plots.");return}const g=Ge(),c=Je(g,v),ae=xe.find(t=>t.testNumber===v),U=ae?.unit,z=ae?.limitLow,F=ae?.limitHigh;if(c.every(t=>t.count===0)){Ie(d,"No parametric test data available for box plots.");return}const p=a.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",d.appendChild(p);const Te=L*2+Math.min(c.length,bt)*(M+k)+He;d.style.maxHeight=`${Te}px`,d.style.overflowY="auto",d.style.scrollbarGutter="stable";let O=-1;const re=c.filter(t=>t.count>0),ye=Math.min(...re.map(t=>t.min)),ve=Math.max(...re.map(t=>t.max)),we=n??dt(ye,ve,z,F);ie.sync(we,z!==void 0||F!==void 0);const W=mt({dataMin:ye,dataMax:ve,limitLow:z,limitHigh:F,includeLimits:we,clipOutliers:u,values:re.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});Y=W.clippedCount,Q();const R=W.lo,ce=W.hi,Le=ce-R||1,V=te&&R>0,fe=V?Math.log10(R):0,Me=(V?Math.log10(ce):0)-fe||1,ue=ut(Math.max(Math.abs(R),Math.abs(ce)),U);function Ye(){const t=L+Re,i=p.clientWidth-t-ht-L;return{plotX:t,plotMaxWidth:Math.max(10,i)}}function S(t,i,s){return V?i+(Math.log10(t)-fe)/Me*s:i+(t-R)/Le*s}function Qe(){const t=[];for(let i=0;i<=4;i++)t.push(V?Math.pow(10,fe+Me*i/4):R+Le*i/4);return t}function j(){const t=ft(a),i=d.clientWidth,s=L*2+c.length*(M+k)+He,A=gt(p,a,i,s);if(!A)return;const{ctx:e}=A;e.font=`${X(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:f}=Ye();c.forEach((o,b)=>{const y=L+b*(M+k),r=y+M/2;b===O&&(e.fillStyle=t.bgHover,e.fillRect(0,y-k/2,i,M+k)),e.fillStyle=t.text,e.textAlign="right";const ze=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(ze,L+Re-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const Fe=l+f,P=Ke=>Math.min(Fe,Math.max(l,Ke)),Se=S(o.min,l,f),Ae=S(o.max,l,f),$=P(Se),_=P(S(o.q1,l,f)),$e=P(S(o.median,l,f)),me=P(S(o.q3,l,f)),B=P(Ae),Ve=Se<$,je=Ae>B,D=y+3,G=y+M-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo($,r),e.lineTo(_,r),e.moveTo(me,r),e.lineTo(B,r);const w=3;Ve?(e.moveTo($+w,r-w),e.lineTo($,r),e.lineTo($+w,r+w)):(e.moveTo($,D),e.lineTo($,G)),je?(e.moveTo(B-w,r-w),e.lineTo(B,r),e.lineTo(B-w,r+w)):(e.moveTo(B,D),e.lineTo(B,G)),e.stroke(),e.fillStyle=De,e.globalAlpha=.35,e.fillRect(_,D,Math.max(1,me-_),G-D),e.globalAlpha=1,e.strokeStyle=De,e.strokeRect(_,D,Math.max(1,me-_),G-D),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo($e,D),e.lineTo($e,G),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(de(o.median,U,"engineering"),l+f+10,r)});const T=L+c.length*(M+k);e.font=`${X(-1)}px system-ui, sans-serif`,xt(e,W.offAxis,{left:l,right:l+f,top:0,bottom:T},"horizontal",t.limitLine,o=>de(o,U,"engineering"));const Ue=new Set(W.offAxis.map(o=>o.value));for(const[o,b]of[[z,"LSL"],[F,"USL"]]){if(o===void 0||Ue.has(o))continue;const y=S(o,l,f);e.strokeStyle=t.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(y,0),e.lineTo(y,T),e.stroke(),e.setLineDash([]),e.fillStyle=t.limitLine;const r=pt(y,e.measureText(b).width,l,l+f,b==="LSL");e.textAlign=r<0?"right":"left",e.textBaseline="bottom",e.fillText(b,y+r*3,T-1)}e.font=`${X(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,T),e.lineTo(l+f,T),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";for(const o of Qe()){const b=S(o,l,f);e.beginPath(),e.moveTo(b,T),e.lineTo(b,T+4),e.stroke(),e.fillText(ue.tick(o),b,T+6)}ue.unitLabel&&(e.textAlign="left",e.font=`${X(-1)}px system-ui, sans-serif`,e.fillText(`(${ue.unitLabel})`,l+f+22,T+6),e.font=`${X(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function ke(t){const i=Math.floor((t-L+k/2)/(M+k));return i>=0&&i<c.length?i:-1}function E(t){return de(t,U,"engineering")}const K=!!m&&m.length>0&&x===null,Ce=t=>!K&&!!N&&g[t]?.waferIndex!==void 0;p.addEventListener("mousemove",t=>{const i=p.getBoundingClientRect(),s=ke(t.clientY-i.top),A=s>=0&&c[s].count>0&&(K||Ce(s));if(s!==O&&(O=s,p.style.cursor=A?"pointer":"default",j()),s>=0&&c[s].count>0){const e=c[s],l=K?`<br><em>click to see this ${Z} by wafer</em>`:Ce(s)?`<br><em>click to ${Pe}</em>`:"";H.innerHTML=`<strong>${e.label}</strong> (${e.count} dies)<br>max ${E(e.max)}<br>q3 ${E(e.q3)}<br>median ${E(e.median)}<br>q1 ${E(e.q1)}<br>min ${E(e.min)}${l}`,H.style.display="block",it(H,a,t.clientX,t.clientY)}else H.style.display="none"}),p.addEventListener("mouseleave",()=>{O!==-1&&(O=-1,j()),H.style.display="none"}),p.addEventListener("click",t=>{const i=p.getBoundingClientRect(),s=ke(t.clientY-i.top);if(s===-1||c[s].count===0)return;if(K){const e=c[s].label;ge(e),h.onGroupChange?.(e);return}const A=g[s]?.waferIndex;N&&A!==void 0&&v!==null&&N(A,v)}),se?.disconnect(),se=nt(a,()=>j()),j(),ct(a,d,Te)}C();function Ne(n){be.set(n)&&(v=n,C())}function qe(n){ie.set(n)&&C()}return{card:a,setTest:Ne,setAxisPrefs:qe,destroy:()=>se?.disconnect()}}
1
+ import{buildTestBoxplotData as Ze}from"../../stats/boxplot.js";import{SPACE as De,fontPx as N,FONT as et,CLR as tt}from"../toolbar.js";import{fmt as xe}from"../../renderer/fmt.js";import{QUANTITY as Ie}from"./palette.js";import{cardShell as nt,observeResize as ot,makeTooltip as it,positionChartTooltip as lt,makeBackButton as st,makeLinkedTestSelect as at,makeToggle as rt,makeLinkedAxisPrefs as ct,renderEmptyState as He,growCardToFitContent as ft,resolveChartCanvasColors as mt,makeAxisFormat as ut,resolveAxisRange as dt,shouldIncludeLimitsByDefault as xt,drawOffAxisLimits as pt,limitLabelSide as ht,PADDING as L,VALUE_WIDTH as gt,prepareCanvas as bt}from"./chartShell.js";import{escHtml as M}from"../../core/utils.js";const k=24,C=5,Re=110,Tt=12,Oe=20;export function renderBoxplotPanel(h){const{title:Z="Test value distribution",items:We,testDefs:pe,onSaveImage:Ee,groups:u,groupLabelText:ee="group",onOpen:q}=h,Pe=h.openActionLabel??"open that wafer",_e=h.openActionLabel??"open this wafer",{card:a,heading:Ge,body:d,controlsRow:Y}=nt(Z,Ee,h.ownerDocument);a.style.flex="0 0 auto",d.style.flex="0 0 auto",a.style.alignSelf="start";const te=pe.filter(n=>n.testNumber!==void 0);let v=h.selectedTestNumber??te[0]?.testNumber??null,ne=!1,Q=0,x=null,H=null,oe=null,he=null;function Xe(){const n=!u||u.length===0?"":x??"\0overview";if(oe&&he===n)return oe;const m=!u||u.length===0?We:x!==null?u.find(g=>g.key===x)?.items??[]:u.map(g=>({label:g.key,dies:g.items.flatMap(c=>c.dies??[])}));return oe=m,he=n,m}function ge(){x!==null&&!H?(H=st(()=>{be(null),h.onGroupChange?.(null)},a.ownerDocument),Y.appendChild(H)):x===null&&H&&(H.remove(),H=null),Ge.textContent=x!==null?`${Z} \u2014 ${ee}: ${x}`:Z}function be(n){return x===n||n!==null&&!(u??[]).some(m=>m.key===n)?!1:(x=n,ge(),U(),S(),!0)}const Te=at(te,v,n=>{v=n,S(),h.onTestChange?.(n)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Ne=Te.el;Y.appendChild(Ne),Y.appendChild(rt("Log scale",ne,n=>{ne=n,S()},a.ownerDocument));const ie=a.ownerDocument.createElement("span");Object.assign(ie.style,{display:"inline-flex",gap:De.lg,alignItems:"center"}),Y.appendChild(ie);const le=ct(ie,h.axisPrefs,n=>{h.onAxisPrefsChange?.(n),S()},a.ownerDocument),se=a.ownerDocument.createElement("div");Object.assign(se.style,{color:tt.label,fontSize:et.body,marginBottom:De.sm}),a.insertBefore(se,d);function U(){const n=!!u&&u.length>0&&x===null,m=[];n?m.push(`click a ${ee}'s box to see it by wafer`):q&&m.push(`click a box to ${Pe}`);const g=m.length?`${m[0][0].toUpperCase()}${m[0].slice(1)} \xB7 `:"";se.textContent=`${g}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(Q?` \xB7 axis clipped, ${Q} value${Q===1?"":"s"} outside`:"")}U(),ge();const O=it(a);let ae=null;function S(){const{includeLimits:n,clipOutliers:m}=le.get();if(U(),d.innerHTML="",te.length===0||v===null){He(d,"No parametric test data available for box plots.");return}const g=Xe(),c=Ze(g,v),re=pe.find(t=>t.testNumber===v),z=re?.unit,F=re?.limitLow,V=re?.limitHigh;if(c.every(t=>t.count===0)){He(d,"No parametric test data available for box plots.");return}const p=a.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",d.appendChild(p);const ye=L*2+Math.min(c.length,Tt)*(k+C)+Oe;d.style.maxHeight=`${ye}px`,d.style.overflowY="auto",d.style.scrollbarGutter="stable";let W=-1;const ce=c.filter(t=>t.count>0),ve=Math.min(...ce.map(t=>t.min)),we=Math.max(...ce.map(t=>t.max)),Le=n??xt(ve,we,F,V);le.sync(Le,F!==void 0||V!==void 0);const E=dt({dataMin:ve,dataMax:we,limitLow:F,limitHigh:V,includeLimits:Le,clipOutliers:m,values:ce.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});Q=E.clippedCount,U();const R=E.lo,fe=E.hi,Me=fe-R||1,j=ne&&R>0,me=j?Math.log10(R):0,ke=(j?Math.log10(fe):0)-me||1,ue=ut(Math.max(Math.abs(R),Math.abs(fe)),z);function Qe(){const t=L+Re,i=p.clientWidth-t-gt-L;return{plotX:t,plotMaxWidth:Math.max(10,i)}}function A(t,i,s){return j?i+(Math.log10(t)-me)/ke*s:i+(t-R)/Me*s}function Ue(){const t=[];for(let i=0;i<=4;i++)t.push(j?Math.pow(10,me+ke*i/4):R+Me*i/4);return t}function K(){const t=mt(a),i=d.clientWidth,s=L*2+c.length*(k+C)+Oe,$=bt(p,a,i,s);if(!$)return;const{ctx:e}=$;e.font=`${N(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:f}=Qe();c.forEach((o,b)=>{const y=L+b*(k+C),r=y+k/2;b===W&&(e.fillStyle=t.bgHover,e.fillRect(0,y-C/2,i,k+C)),e.fillStyle=t.text,e.textAlign="right";const Fe=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(Fe,L+Re-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const Ve=l+f,_=Je=>Math.min(Ve,Math.max(l,Je)),Ae=A(o.min,l,f),$e=A(o.max,l,f),B=_(Ae),G=_(A(o.q1,l,f)),Be=_(A(o.median,l,f)),de=_(A(o.q3,l,f)),D=_($e),je=Ae<B,Ke=$e>D,I=y+3,X=y+k-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(B,r),e.lineTo(G,r),e.moveTo(de,r),e.lineTo(D,r);const w=3;je?(e.moveTo(B+w,r-w),e.lineTo(B,r),e.lineTo(B+w,r+w)):(e.moveTo(B,I),e.lineTo(B,X)),Ke?(e.moveTo(D-w,r-w),e.lineTo(D,r),e.lineTo(D-w,r+w)):(e.moveTo(D,I),e.lineTo(D,X)),e.stroke(),e.fillStyle=Ie,e.globalAlpha=.35,e.fillRect(G,I,Math.max(1,de-G),X-I),e.globalAlpha=1,e.strokeStyle=Ie,e.strokeRect(G,I,Math.max(1,de-G),X-I),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Be,I),e.lineTo(Be,X),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(xe(o.median,z,"engineering"),l+f+10,r)});const T=L+c.length*(k+C);e.font=`${N(-1)}px system-ui, sans-serif`,pt(e,E.offAxis,{left:l,right:l+f,top:0,bottom:T},"horizontal",t.limitLine,o=>xe(o,z,"engineering"));const ze=new Set(E.offAxis.map(o=>o.value));for(const[o,b]of[[F,"LSL"],[V,"USL"]]){if(o===void 0||ze.has(o))continue;const y=A(o,l,f);e.strokeStyle=t.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(y,0),e.lineTo(y,T),e.stroke(),e.setLineDash([]),e.fillStyle=t.limitLine;const r=ht(y,e.measureText(b).width,l,l+f,b==="LSL");e.textAlign=r<0?"right":"left",e.textBaseline="bottom",e.fillText(b,y+r*3,T-1)}e.font=`${N(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,T),e.lineTo(l+f,T),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";for(const o of Ue()){const b=A(o,l,f);e.beginPath(),e.moveTo(b,T),e.lineTo(b,T+4),e.stroke(),e.fillText(ue.tick(o),b,T+6)}ue.unitLabel&&(e.textAlign="left",e.font=`${N(-1)}px system-ui, sans-serif`,e.fillText(`(${ue.unitLabel})`,l+f+22,T+6),e.font=`${N(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function Ce(t){const i=Math.floor((t-L+C/2)/(k+C));return i>=0&&i<c.length?i:-1}function P(t){return xe(t,z,"engineering")}const J=!!u&&u.length>0&&x===null,Se=t=>!J&&!!q&&g[t]?.waferIndex!==void 0;p.addEventListener("mousemove",t=>{const i=p.getBoundingClientRect(),s=Ce(t.clientY-i.top),$=s>=0&&c[s].count>0&&(J||Se(s));if(s!==W&&(W=s,p.style.cursor=$?"pointer":"default",K()),s>=0&&c[s].count>0){const e=c[s],l=J?`<br><em>click to see this ${M(ee)} by wafer</em>`:Se(s)?`<br><em>click to ${M(_e)}</em>`:"";O.innerHTML=`<strong>${M(e.label)}</strong> (${e.count} dies)<br>max ${M(P(e.max))}<br>q3 ${M(P(e.q3))}<br>median ${M(P(e.median))}<br>q1 ${M(P(e.q1))}<br>min ${M(P(e.min))}${l}`,O.style.display="block",lt(O,a,t.clientX,t.clientY)}else O.style.display="none"}),p.addEventListener("mouseleave",()=>{W!==-1&&(W=-1,K()),O.style.display="none"}),p.addEventListener("click",t=>{const i=p.getBoundingClientRect(),s=Ce(t.clientY-i.top);if(s===-1||c[s].count===0)return;if(J){const e=c[s].label;be(e),h.onGroupChange?.(e);return}const $=g[s]?.waferIndex;q&&$!==void 0&&v!==null&&q($,v)}),ae?.disconnect(),ae=ot(a,()=>K()),K(),ft(a,d,ye)}S();function qe(n){Te.set(n)&&(v=n,S())}function Ye(n){le.set(n)&&S()}return{card:a,setTest:qe,setAxisPrefs:Ye,destroy:()=>ae?.disconnect()}}
@@ -1 +1 @@
1
- import{buildCapabilityData as fe}from"../../stats/capability.js";import{capabilityColor as B}from"./palette.js";import{LEADING as ue,SPACE as g,fontPx as G,FONT as Q,CLR as H}from"../toolbar.js";import{cardShell as be,chartFillHeight as ye,applyCanvasFlow as xe,observeResize as ge,makeTooltip as ve,positionChartTooltip as Se,renderEmptyState as Me,resolveChartCanvasColors as Te,chartSwatchCss as Le,prepareCanvas as ke,chartDpr as Ce}from"./chartShell.js";import{fmt as Pe}from"../../renderer/fmt.js";const $e=30,Ae=160,ee=90,te=28,v=42;function I(C){return C===null?"\u2014":C.toFixed(2)}export function renderCapabilityPanel(C){const{title:ne="Process capability",items:oe,testDefs:z,onSaveImage:le,onSelectTest:O}=C;let X=C.selectedTestNumber??null;const{card:r,body:S}=be(ne,le,C.ownerDocument),se=z.some(n=>n.testNumber!==void 0);S.style.overflowX="auto";function ie(){return oe}const P=r.ownerDocument.createElement("div");Object.assign(P.style,{display:"flex",flexDirection:"column",gap:g.xs,marginBottom:g.sm}),r.insertBefore(P,S);const W=ve(r);let R=()=>{};function ae(n,i,f){P.innerHTML="";const c=r.ownerDocument.createElement("span");Object.assign(c.style,{display:"inline-flex",alignItems:"center",gap:g.sm,color:H.value,fontSize:Q.body,fontWeight:"500"});const M=f-n,b=i>0?` \xB7 ${i} without spec limits`:"",y=r.ownerDocument.createElement("span");y.textContent=M>0?`${n} of ${f} tests shown${b} \xB7 ${M} excluded (no recorded values)`:`${n} test${n!==1?"s":""} shown${b}`,c.appendChild(y),P.appendChild(c);const $=i>=n,T=r.ownerDocument.createElement("div");T.textContent=$?"No test has both spec limits, so each is normalised to its own observed range (min = 0, max = 1) and sorted most-variable first. Ppk needs limits, so none is shown.":"Normalised to spec limits (LSL = 0, USL = 1), worst Ppk first. Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(T.style,{color:H.label,fontSize:Q.body,lineHeight:ue.base,marginTop:g.xs,maxWidth:"78ch"}),P.appendChild(T),P.appendChild(re(r.ownerDocument,i>0,$))}function re(n,i,f=!1){const c=n.createElement("div");Object.assign(c.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${g.xs} ${g.xl}`,marginTop:g.xl,marginBottom:g.xs,fontSize:Q.body,color:H.label});const M=f?[]:[{color:B(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:B(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:B(.5),label:"Poor \xB7 Ppk < 1.0"}];i&&M.push({color:H.label,label:"No spec limits \xB7 no Ppk",dashed:!0});for(const b of M){const y=n.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:g.sm});const $=n.createElement("span");$.style.cssText=Le(b.color,b.dashed?"outline":"solid");const T=n.createElement("span");T.textContent=b.label,y.append($,T),c.appendChild(y)}return c}function ce(n){if(S.innerHTML="",n.length===0)return Me(S,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const i=r.ownerDocument.createElement("canvas");i.style.display="block",i.style.cursor="default",S.appendChild(i);const f=n.length,c=Math.min(0,...n.map(t=>t.min)),M=Math.max(1,...n.map(t=>t.max)),b=(M-c)*.05||.1,y=c-b,T=M+b-y||1;function V(t){return Math.max($e,Math.min(Ae,Math.floor(t/f)))}function L(t,s,o){return s+(1-(t-y)/T)*o}function pe(t,s,o,l,k){const x=n.filter(a=>a.hasSpec).length,A=x===n.length,d=x===0,_=d?"max":"USL",e=d?"min":"LSL",h=A?"normalised to spec":d?"normalised to range":"normalised (per test)";t.font=`${G(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle";for(const[a,D]of[[1,_],[0,e]]){const p=L(a,o,l);p<o-1||p>o+l+1||(t.strokeStyle=s.limitLine,t.lineWidth=1,t.beginPath(),t.moveTo(v-5,p),t.lineTo(v,p),t.stroke(),t.fillStyle=s.textMuted,t.fillText(D,v-8,p))}t.save(),t.translate(9,o+l/2),t.rotate(-Math.PI/2),t.textAlign="center",t.fillStyle=s.textMuted,t.fillText(h,0,0),t.restore()}let w=-1;function j(){xe(i);const t=S.clientWidth,s=V(t-v),o=s*f,l=te,k=v+o,x=ye(r,S,i,te+200+ee),A=Math.max(l+60,x-ee),d=A-l,_=ke(i,r,k,x);if(!_)return;const{ctx:e}=_,h=Te(r);n.forEach((a,D)=>{const p=v+D*s,m=p+s/2,u=Math.max(4,s*.55);if(D===w&&(e.fillStyle=h.bgHover,e.fillRect(p,l,s,A-l)),n[D].testNumber===X&&(e.fillStyle=h.accent,e.fillRect(p,A-2,s,2)),a.hasSpec){const K=L(0,l,d),Z=L(1,l,d);e.strokeStyle=h.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(p,K),e.lineTo(p+s,K),e.moveTo(p,Z),e.lineTo(p+s,Z),e.stroke(),e.setLineDash([])}const F=a.hasSpec?B(a.ppk):h.textMuted,U=L(a.min,l,d),E=L(a.q1,l,d),J=L(a.median,l,d),N=L(a.q3,l,d),q=L(a.max,l,d);e.strokeStyle=h.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(m,U),e.lineTo(m,E),e.moveTo(m,N),e.lineTo(m,q),e.moveTo(m-u/4,U),e.lineTo(m+u/4,U),e.moveTo(m-u/4,q),e.lineTo(m+u/4,q),e.stroke(),e.fillStyle=F,e.globalAlpha=.5,e.fillRect(m-u/2,Math.min(E,N),u,Math.max(1,Math.abs(N-E))),e.globalAlpha=1,e.strokeStyle=F,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(m-u/2,Math.min(E,N),u,Math.max(1,Math.abs(N-E))),e.setLineDash([]),e.strokeStyle=h.text,e.lineWidth=2,e.beginPath(),e.moveTo(m-u/2,J),e.lineTo(m+u/2,J),e.stroke(),e.lineWidth=1,e.font=`${G(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?F:h.textMuted,e.fillText(a.hasSpec?I(a.ppk):"\u2014",m,l-8);const he=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(m,A+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${G(-1)}px system-ui, sans-serif`,e.fillStyle=h.textMuted,e.fillText(he,0,0),e.restore()}),e.strokeStyle=h.border,e.lineWidth=1,e.strokeRect(v,l,o,d),pe(e,h,l,d,o)}function Y(t){const s=i.getBoundingClientRect(),o=S.clientWidth,l=V(o-v),k=(t.clientX-s.left)*(i.width/Ce(i)/s.width)-v,x=Math.floor(k/l);return x>=0&&x<f?x:-1}return i.addEventListener("mousemove",t=>{const s=Y(t);if(s!==w&&(w=s,j()),s===-1){W.style.display="none";return}const o=n[s],l=k=>Pe(k,o.unit);W.innerHTML=`<strong>${o.label}</strong> (n=${o.n})<br>`+(o.hasSpec?`LSL ${l(o.lsl)} \xB7 USL ${l(o.usl)}<br>mean ${l(o.mean)}<br>Cp ${I(o.cp)} \xB7 Cpk ${I(o.cpk)}<br>Pp ${I(o.pp)} \xB7 Ppk ${I(o.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${l(o.mean)} \xB7 stddev ${l(o.stdOverall)}`)+(O?"<br><em>click to view in boxplot</em>":""),W.style.display="block",Se(W,r,t.clientX,t.clientY),i.style.cursor=O?"pointer":"default"}),i.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,j()),W.style.display="none"}),i.addEventListener("click",t=>{if(!O)return;const s=Y(t);s!==-1&&O(n[s].testNumber)}),j}function me(){const n=fe(ie(),z),i=z.filter(c=>c.testNumber!==void 0).length,f=n.filter(c=>!c.hasSpec).length;ae(n.length,f,i),R=ce(n),R()}const de=ge(r,()=>R());return me(),{card:r,hasData:se,setTest:n=>{X!==n&&(X=n,R())},destroy:()=>de.disconnect()}}
1
+ import{buildCapabilityData as ue}from"../../stats/capability.js";import{capabilityColor as B}from"./palette.js";import{LEADING as be,SPACE as g,fontPx as G,FONT as Q,CLR as H}from"../toolbar.js";import{cardShell as ye,chartFillHeight as xe,applyCanvasFlow as ge,observeResize as ve,makeTooltip as Se,positionChartTooltip as Me,renderEmptyState as Te,resolveChartCanvasColors as Le,chartSwatchCss as ke,prepareCanvas as Ce,chartDpr as Pe}from"./chartShell.js";import{fmt as $e}from"../../renderer/fmt.js";import{escHtml as ee}from"../../core/utils.js";const Ae=30,We=160,te=90,ne=28,v=42;function I(C){return C===null?"\u2014":C.toFixed(2)}export function renderCapabilityPanel(C){const{title:oe="Process capability",items:le,testDefs:z,onSaveImage:se,onSelectTest:O}=C;let X=C.selectedTestNumber??null;const{card:r,body:S}=ye(oe,se,C.ownerDocument),ie=z.some(n=>n.testNumber!==void 0);S.style.overflowX="auto";function ae(){return le}const P=r.ownerDocument.createElement("div");Object.assign(P.style,{display:"flex",flexDirection:"column",gap:g.xs,marginBottom:g.sm}),r.insertBefore(P,S);const W=Se(r);let R=()=>{};function re(n,i,f){P.innerHTML="";const c=r.ownerDocument.createElement("span");Object.assign(c.style,{display:"inline-flex",alignItems:"center",gap:g.sm,color:H.value,fontSize:Q.body,fontWeight:"500"});const M=f-n,b=i>0?` \xB7 ${i} without spec limits`:"",y=r.ownerDocument.createElement("span");y.textContent=M>0?`${n} of ${f} tests shown${b} \xB7 ${M} excluded (no recorded values)`:`${n} test${n!==1?"s":""} shown${b}`,c.appendChild(y),P.appendChild(c);const $=i>=n,T=r.ownerDocument.createElement("div");T.textContent=$?"No test has both spec limits, so each is normalised to its own observed range (min = 0, max = 1) and sorted most-variable first. Ppk needs limits, so none is shown.":"Normalised to spec limits (LSL = 0, USL = 1), worst Ppk first. Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(T.style,{color:H.label,fontSize:Q.body,lineHeight:be.base,marginTop:g.xs,maxWidth:"78ch"}),P.appendChild(T),P.appendChild(ce(r.ownerDocument,i>0,$))}function ce(n,i,f=!1){const c=n.createElement("div");Object.assign(c.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${g.xs} ${g.xl}`,marginTop:g.xl,marginBottom:g.xs,fontSize:Q.body,color:H.label});const M=f?[]:[{color:B(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:B(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:B(.5),label:"Poor \xB7 Ppk < 1.0"}];i&&M.push({color:H.label,label:"No spec limits \xB7 no Ppk",dashed:!0});for(const b of M){const y=n.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:g.sm});const $=n.createElement("span");$.style.cssText=ke(b.color,b.dashed?"outline":"solid");const T=n.createElement("span");T.textContent=b.label,y.append($,T),c.appendChild(y)}return c}function me(n){if(S.innerHTML="",n.length===0)return Te(S,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const i=r.ownerDocument.createElement("canvas");i.style.display="block",i.style.cursor="default",S.appendChild(i);const f=n.length,c=Math.min(0,...n.map(t=>t.min)),M=Math.max(1,...n.map(t=>t.max)),b=(M-c)*.05||.1,y=c-b,T=M+b-y||1;function V(t){return Math.max(Ae,Math.min(We,Math.floor(t/f)))}function L(t,s,o){return s+(1-(t-y)/T)*o}function he(t,s,o,l,k){const x=n.filter(a=>a.hasSpec).length,A=x===n.length,d=x===0,_=d?"max":"USL",e=d?"min":"LSL",h=A?"normalised to spec":d?"normalised to range":"normalised (per test)";t.font=`${G(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle";for(const[a,D]of[[1,_],[0,e]]){const p=L(a,o,l);p<o-1||p>o+l+1||(t.strokeStyle=s.limitLine,t.lineWidth=1,t.beginPath(),t.moveTo(v-5,p),t.lineTo(v,p),t.stroke(),t.fillStyle=s.textMuted,t.fillText(D,v-8,p))}t.save(),t.translate(9,o+l/2),t.rotate(-Math.PI/2),t.textAlign="center",t.fillStyle=s.textMuted,t.fillText(h,0,0),t.restore()}let w=-1;function j(){ge(i);const t=S.clientWidth,s=V(t-v),o=s*f,l=ne,k=v+o,x=xe(r,S,i,ne+200+te),A=Math.max(l+60,x-te),d=A-l,_=Ce(i,r,k,x);if(!_)return;const{ctx:e}=_,h=Le(r);n.forEach((a,D)=>{const p=v+D*s,m=p+s/2,u=Math.max(4,s*.55);if(D===w&&(e.fillStyle=h.bgHover,e.fillRect(p,l,s,A-l)),n[D].testNumber===X&&(e.fillStyle=h.accent,e.fillRect(p,A-2,s,2)),a.hasSpec){const K=L(0,l,d),Z=L(1,l,d);e.strokeStyle=h.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(p,K),e.lineTo(p+s,K),e.moveTo(p,Z),e.lineTo(p+s,Z),e.stroke(),e.setLineDash([])}const F=a.hasSpec?B(a.ppk):h.textMuted,U=L(a.min,l,d),E=L(a.q1,l,d),J=L(a.median,l,d),N=L(a.q3,l,d),q=L(a.max,l,d);e.strokeStyle=h.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(m,U),e.lineTo(m,E),e.moveTo(m,N),e.lineTo(m,q),e.moveTo(m-u/4,U),e.lineTo(m+u/4,U),e.moveTo(m-u/4,q),e.lineTo(m+u/4,q),e.stroke(),e.fillStyle=F,e.globalAlpha=.5,e.fillRect(m-u/2,Math.min(E,N),u,Math.max(1,Math.abs(N-E))),e.globalAlpha=1,e.strokeStyle=F,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(m-u/2,Math.min(E,N),u,Math.max(1,Math.abs(N-E))),e.setLineDash([]),e.strokeStyle=h.text,e.lineWidth=2,e.beginPath(),e.moveTo(m-u/2,J),e.lineTo(m+u/2,J),e.stroke(),e.lineWidth=1,e.font=`${G(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?F:h.textMuted,e.fillText(a.hasSpec?I(a.ppk):"\u2014",m,l-8);const fe=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(m,A+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${G(-1)}px system-ui, sans-serif`,e.fillStyle=h.textMuted,e.fillText(fe,0,0),e.restore()}),e.strokeStyle=h.border,e.lineWidth=1,e.strokeRect(v,l,o,d),he(e,h,l,d,o)}function Y(t){const s=i.getBoundingClientRect(),o=S.clientWidth,l=V(o-v),k=(t.clientX-s.left)*(i.width/Pe(i)/s.width)-v,x=Math.floor(k/l);return x>=0&&x<f?x:-1}return i.addEventListener("mousemove",t=>{const s=Y(t);if(s!==w&&(w=s,j()),s===-1){W.style.display="none";return}const o=n[s],l=k=>ee($e(k,o.unit));W.innerHTML=`<strong>${ee(o.label)}</strong> (n=${o.n})<br>`+(o.hasSpec?`LSL ${l(o.lsl)} \xB7 USL ${l(o.usl)}<br>mean ${l(o.mean)}<br>Cp ${I(o.cp)} \xB7 Cpk ${I(o.cpk)}<br>Pp ${I(o.pp)} \xB7 Ppk ${I(o.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${l(o.mean)} \xB7 stddev ${l(o.stdOverall)}`)+(O?"<br><em>click to view in boxplot</em>":""),W.style.display="block",Me(W,r,t.clientX,t.clientY),i.style.cursor=O?"pointer":"default"}),i.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,j()),W.style.display="none"}),i.addEventListener("click",t=>{if(!O)return;const s=Y(t);s!==-1&&O(n[s].testNumber)}),j}function de(){const n=ue(ae(),z),i=z.filter(c=>c.testNumber!==void 0).length,f=n.filter(c=>!c.hasSpec).length;re(n.length,f,i),R=me(n),R()}const pe=ve(r,()=>R());return de(),{card:r,hasData:ie,setTest:n=>{X!==n&&(X=n,R())},destroy:()=>pe.disconnect()}}
@@ -1,2 +1,2 @@
1
- import{buildCorrelationMatrix as yt,filterCorrelationMatrix as Mt}from"../../stats/correlation.js";import{csvField as rt}from"../../core/utils.js";import{CORRELATION_POSITIVE as it,CORRELATION_NEGATIVE as at}from"./palette.js";import{buildFacetTable as wt}from"../../stats/facets.js";import{wireControlHover as vt,controlStyle as Lt,SPACE as C,RADIUS as tt,fontPx as ct,FONT as z,CLR as b,saveTextFile as Ct}from"../toolbar.js";import{attachChartTip as It,cardShell as Tt,observeResize as St,makeTooltip as Et,positionChartTooltip as $t,makeWaferSelect as Rt,renderEmptyState as Dt,resolveChartCanvasColors as kt,prepareCanvas as At,chartDpr as dt}from"./chartShell.js";const ut=5,ft=100,pt=20;function et(x){const d=x.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(d)return[+d[1],+d[2],+d[3]];const f=x.trim().replace("#","");return f.length===6?[parseInt(f.slice(0,2),16),parseInt(f.slice(2,4),16),parseInt(f.slice(4,6),16)]:null}function Bt(x){const d=et(x);return d?(d[0]*299+d[1]*587+d[2]*114)/1e3>140?"#111":"#fff":"#000"}function Ot(x,d,f){const B=et(x);if(!B)return x;const G=Math.round(d[0]+(B[0]-d[0])*f),O=Math.round(d[1]+(B[1]-d[1])*f),l=Math.round(d[2]+(B[2]-d[2])*f);return`rgb(${G},${O},${l})`}export function renderCorrelationPanel(x){const{title:d="Test correlation matrix",items:f,testDefs:B,onSaveImage:G,onSelectPair:O}=x,{card:l,body:D,controlsRow:U}=Tt(d,G,x.ownerDocument);D.style.overflowX="auto",l.style.alignSelf="start";let Y=pt,j=null;const H=l.ownerDocument.createElement("label");H.textContent="Max tests:",Object.assign(H.style,{color:b.label,fontSize:z.body,display:"flex",alignItems:"center",gap:C.xs});const L=l.ownerDocument.createElement("input");L.type="number",L.min=String(ut),L.max=String(ft),L.value=String(Y),Object.assign(L.style,{width:"52px",fontSize:z.body,padding:`${C.xxs} ${C.xs}`,background:b.menuBg,color:b.value,border:`1px solid ${b.menuBorder}`,borderRadius:tt.control}),L.addEventListener("change",()=>{const e=Math.max(ut,Math.min(ft,parseInt(L.value,10)||pt));L.value=String(e),e!==Y&&(Y=e,J())}),H.appendChild(L),U.appendChild(H),f.length>1&&U.appendChild(Rt(f,j,e=>{j=e,J()},{ownerDocument:l.ownerDocument}));function mt(){return j!==null?f[j]?[f[j]]:[]:f}if(x.onSaveText){const e=l.ownerDocument.createElement("button");e.type="button",e.textContent="Correlation CSV",Object.assign(e.style,{...Lt("outlined"),color:b.text}),vt(e),e.addEventListener("click",()=>{const o=nt;if(!o)return;const u=["Test X,Test X number,Test Y,Test Y number,r,n"];for(let r=0;r<o.tests.length;r++)for(let p=r+1;p<o.tests.length;p++){const i=o.cells.find(m=>m.xIndex===p&&m.yIndex===r);i&&u.push([rt(o.tests[p].label),String(o.tests[p].testNumber),rt(o.tests[r].label),String(o.tests[r].testNumber),i.r===null?"":i.r.toFixed(6),String(i.n)].join(","))}Ct(u.join(`
2
- `),"test-correlation.csv","text/csv",x.onSaveText)}),U.appendChild(e)}const _=l.ownerDocument.createElement("div");Object.assign(_.style,{display:"flex",flexDirection:"column",gap:C.xs,marginBottom:C.sm}),l.insertBefore(_,D);const I=Et(l);It(H,l,I,"Cap on how many tests the matrix includes (strongest correlations kept first)");let q=()=>{};function ht(e,o,u,r,p,i){if(_.innerHTML="",p.length>0){const w=l.ownerDocument.createElement("div");w.textContent=`\u26A0 Mixed ${p.join(", ")} within this set \u2014 correlations may be misleading (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,Object.assign(w.style,{color:b.warnText,background:b.warnBg,border:`1px solid ${b.warnBorder}`,borderRadius:tt.control,padding:`${C.xs} ${C.md}`,fontSize:z.body}),_.appendChild(w)}const m=l.ownerDocument.createElement("span");Object.assign(m.style,{display:"inline-flex",alignItems:"center",gap:C.sm,color:b.label,fontSize:z.body,flexWrap:"wrap"});const g=l.ownerDocument.createElement("span");g.textContent=i!==null?`Pearson r \xB7 n \u2248 ${i.toLocaleString()} dies per pair \xB7 click a cell to view that pair in scatter \xB7`:"Pearson r \xB7 click a cell to view that pair in scatter \xB7",m.appendChild(g);const k=l.ownerDocument.createElement("span");Object.assign(k.style,{display:"inline-flex",alignItems:"center",gap:C.xs});const T=l.ownerDocument.createElement("span");T.textContent="\u22121";const N=l.ownerDocument.createElement("span");Object.assign(N.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:tt.control,border:`1px solid ${b.menuBorder}`,background:`linear-gradient(to right, ${at}, ${b.menuBg}, ${it})`});const a=l.ownerDocument.createElement("span");a.textContent="+1",k.append(T,N,a),m.appendChild(k),_.appendChild(m);const M=l.ownerDocument.createElement("span");if(Object.assign(M.style,{color:b.value,fontSize:z.body,fontWeight:"500"}),e===0&&o===0)M.textContent=r?`No significant correlations found \u2014 strongest pair: ${r.xLabel} \u2194 ${r.yLabel} (r = ${r.r.toFixed(2)})`:"No significant correlations found";else{const w=[];e>0&&w.push(`${e} strong (|r| \u2265 0.7)`),o>0&&w.push(`${o} moderate (0.4\u20130.7)`);const F=e+o;let X=w.join(", ")+` pair${F!==1?"s":""} found`;u>0&&(X+=` \xB7 ${u} weak pair${u!==1?"s":""} not shown`),M.textContent=X}_.appendChild(M)}function xt(e){if(D.innerHTML="",e.tests.length<2)return Dt(D,"Need at least two parametric tests for a correlation matrix."),()=>{};const o=l.ownerDocument.createElement("canvas");o.style.display="block",o.style.cursor="default",D.appendChild(o);const u=e.tests.length,r=s=>s.label.split(" (#")[0],p=Math.min(14,Math.max(...e.tests.map(s=>r(s).length))),i=p*6.5+8,m=10,g=Math.round(m*6.5*Math.sin(Math.PI/4))+14,k=14,T=28,N=44;let a=-1,M=-1;function w(s){const c=Math.max(0,s-i);return Math.max(k,Math.min(N,Math.floor(c/u)))}const F=new Map;for(const s of e.cells){let c=F.get(s.yIndex);c||(c=[],F.set(s.yIndex,c)),c.push(s)}function X(){const s=kt(l),c=et(s.bg)??[255,255,255],h=D.clientWidth,n=w(h),W=n*u,S=g+n*u+4,E=i+W,$=At(o,l,E,S);if(!$)return;const{ctx:t}=$;t.font=`${ct(-1)}px system-ui, sans-serif`,t.fillStyle=s.text,e.tests.forEach((R,y)=>{const v=r(R),A=v.length>m?`${v.slice(0,m-1)}\u2026`:v,K=i+y*n+n/2;t.save(),t.translate(K,g-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=y===a?s.text:s.textMuted,t.fillText(A,0,0),t.restore()}),e.tests.forEach((R,y)=>{const v=r(R),A=g+y*n,K=A+n/2;t.font=`${ct(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle",t.fillStyle=y===M?s.text:s.textMuted,t.fillText(v.length>p?`${v.slice(0,p-1)}\u2026`:v,i-4,K),(F.get(y)??[]).forEach(st=>{const Q=st.xIndex,P=i+Q*n,bt=Q===a&&y===M,lt=Q===y,V=st.r;if(V===null){t.fillStyle=s.border,t.globalAlpha=.4,t.fillRect(P+1,A+1,n-2,n-2),t.globalAlpha=1;return}let Z=s.bgHover;lt?t.fillStyle=s.bgHover:(Z=Ot(V>=0?it:at,c,Math.abs(V)),t.fillStyle=Z),t.fillRect(P+1,A+1,n-2,n-2),n>=T&&!lt&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Bt(Z),t.textAlign="center",t.textBaseline="middle",t.fillText(V.toFixed(2),P+n/2,A+n/2)),bt&&(t.strokeStyle=s.text,t.lineWidth=2,t.strokeRect(P+1,A+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=s.bg,t.lineWidth=1;for(let R=0;R<=u;R++){const y=i+R*n;t.beginPath(),t.moveTo(y,g),t.lineTo(y,g+u*n),t.stroke();const v=g+R*n;t.beginPath(),t.moveTo(i,v),t.lineTo(i+u*n,v),t.stroke()}}function ot(s){const c=o.getBoundingClientRect(),h=D.clientWidth,n=w(h),W=(s.clientX-c.left)*(o.width/dt(o)/c.width),S=(s.clientY-c.top)*(o.height/dt(o)/c.height),E=Math.floor((W-i)/n),$=Math.floor((S-g)/n);return E>=0&&E<u&&$>=0&&$<u?{xi:E,yi:$}:null}return o.addEventListener("mousemove",s=>{const c=ot(s);if(!c){I.style.display="none",o.style.cursor="default";return}const{xi:h,yi:n}=c,W=h===n;o.style.cursor=W||!O?"default":"pointer";const S=e.cells.find(t=>t.xIndex===h&&t.yIndex===n),E=e.tests[h].label,$=e.tests[n].label;W?I.innerHTML=`<strong>${E}</strong>`:S?.r!==null&&S?.r!==void 0?I.innerHTML=`<strong>${r(e.tests[n])}</strong> (#${e.tests[n].testNumber}) vs <strong>${r(e.tests[h])}</strong> (#${e.tests[h].testNumber})<br>r = ${S.r.toFixed(4)} \xB7 n = ${S.n.toLocaleString()}${O?"<br><em>click to view in scatter</em>":""}`:I.innerHTML=`${$} vs ${E}<br><em>insufficient data</em>`,I.style.display="block",$t(I,l,s.clientX,s.clientY)}),o.addEventListener("mouseleave",()=>{I.style.display="none"}),o.addEventListener("click",s=>{const c=ot(s);if(!c||!O)return;const{xi:h,yi:n}=c;h!==n&&(a=h,M=n,X(),O(e.tests[h].testNumber,e.tests[n].testNumber))}),X}let nt=null;function J(){const e=mt(),o=e.flatMap(a=>a.dies??[]),u=yt(o,B),{matrix:r,strongPairs:p,moderatePairs:i,hiddenWeakPairs:m,strongestPair:g}=Mt(u,{minTests:6,maxTests:Y}),k=wt(e,{facetableOnly:!0}).filter(a=>a.splittable).map(a=>a.label),T=r.cells.filter(a=>a.xIndex!==a.yIndex).map(a=>a.n).sort((a,M)=>a-M),N=T.length?T[Math.floor(T.length/2)]:null;nt=r,ht(p,i,m,g,k,N),q=xt(r),q()}const gt=St(l,()=>q());return J(),{card:l,destroy:()=>gt.disconnect()}}
1
+ import{buildCorrelationMatrix as Mt,filterCorrelationMatrix as wt}from"../../stats/correlation.js";import{csvField as it}from"../../core/utils.js";import{CORRELATION_POSITIVE as at,CORRELATION_NEGATIVE as ct}from"./palette.js";import{buildFacetTable as vt}from"../../stats/facets.js";import{wireControlHover as Lt,controlStyle as Ct,SPACE as C,RADIUS as et,fontPx as dt,FONT as z,CLR as b,saveTextFile as It}from"../toolbar.js";import{attachChartTip as Tt,cardShell as St,observeResize as $t,makeTooltip as Et,positionChartTooltip as Rt,makeWaferSelect as Dt,renderEmptyState as kt,resolveChartCanvasColors as At,prepareCanvas as Bt,chartDpr as ut}from"./chartShell.js";import{escHtml as G}from"../../core/utils.js";const ft=5,pt=100,mt=20;function nt(x){const d=x.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(d)return[+d[1],+d[2],+d[3]];const f=x.trim().replace("#","");return f.length===6?[parseInt(f.slice(0,2),16),parseInt(f.slice(2,4),16),parseInt(f.slice(4,6),16)]:null}function Ot(x){const d=nt(x);return d?(d[0]*299+d[1]*587+d[2]*114)/1e3>140?"#111":"#fff":"#000"}function _t(x,d,f){const B=nt(x);if(!B)return x;const U=Math.round(d[0]+(B[0]-d[0])*f),O=Math.round(d[1]+(B[1]-d[1])*f),l=Math.round(d[2]+(B[2]-d[2])*f);return`rgb(${U},${O},${l})`}export function renderCorrelationPanel(x){const{title:d="Test correlation matrix",items:f,testDefs:B,onSaveImage:U,onSelectPair:O}=x,{card:l,body:D,controlsRow:q}=St(d,U,x.ownerDocument);D.style.overflowX="auto",l.style.alignSelf="start";let Y=mt,j=null;const H=l.ownerDocument.createElement("label");H.textContent="Max tests:",Object.assign(H.style,{color:b.label,fontSize:z.body,display:"flex",alignItems:"center",gap:C.xs});const L=l.ownerDocument.createElement("input");L.type="number",L.min=String(ft),L.max=String(pt),L.value=String(Y),Object.assign(L.style,{width:"52px",fontSize:z.body,padding:`${C.xxs} ${C.xs}`,background:b.menuBg,color:b.value,border:`1px solid ${b.menuBorder}`,borderRadius:et.control}),L.addEventListener("change",()=>{const e=Math.max(ft,Math.min(pt,parseInt(L.value,10)||mt));L.value=String(e),e!==Y&&(Y=e,K())}),H.appendChild(L),q.appendChild(H),f.length>1&&q.appendChild(Dt(f,j,e=>{j=e,K()},{ownerDocument:l.ownerDocument}));function ht(){return j!==null?f[j]?[f[j]]:[]:f}if(x.onSaveText){const e=l.ownerDocument.createElement("button");e.type="button",e.textContent="Correlation CSV",Object.assign(e.style,{...Ct("outlined"),color:b.text}),Lt(e),e.addEventListener("click",()=>{const o=ot;if(!o)return;const u=["Test X,Test X number,Test Y,Test Y number,r,n"];for(let r=0;r<o.tests.length;r++)for(let p=r+1;p<o.tests.length;p++){const i=o.cells.find(m=>m.xIndex===p&&m.yIndex===r);i&&u.push([it(o.tests[p].label),String(o.tests[p].testNumber),it(o.tests[r].label),String(o.tests[r].testNumber),i.r===null?"":i.r.toFixed(6),String(i.n)].join(","))}It(u.join(`
2
+ `),"test-correlation.csv","text/csv",x.onSaveText)}),q.appendChild(e)}const _=l.ownerDocument.createElement("div");Object.assign(_.style,{display:"flex",flexDirection:"column",gap:C.xs,marginBottom:C.sm}),l.insertBefore(_,D);const I=Et(l);Tt(H,l,I,"Cap on how many tests the matrix includes (strongest correlations kept first)");let J=()=>{};function xt(e,o,u,r,p,i){if(_.innerHTML="",p.length>0){const w=l.ownerDocument.createElement("div");w.textContent=`\u26A0 Mixed ${p.join(", ")} within this set \u2014 correlations may be misleading (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,Object.assign(w.style,{color:b.warnText,background:b.warnBg,border:`1px solid ${b.warnBorder}`,borderRadius:et.control,padding:`${C.xs} ${C.md}`,fontSize:z.body}),_.appendChild(w)}const m=l.ownerDocument.createElement("span");Object.assign(m.style,{display:"inline-flex",alignItems:"center",gap:C.sm,color:b.label,fontSize:z.body,flexWrap:"wrap"});const g=l.ownerDocument.createElement("span");g.textContent=i!==null?`Pearson r \xB7 n \u2248 ${i.toLocaleString()} dies per pair \xB7 click a cell to view that pair in scatter \xB7`:"Pearson r \xB7 click a cell to view that pair in scatter \xB7",m.appendChild(g);const k=l.ownerDocument.createElement("span");Object.assign(k.style,{display:"inline-flex",alignItems:"center",gap:C.xs});const T=l.ownerDocument.createElement("span");T.textContent="\u22121";const N=l.ownerDocument.createElement("span");Object.assign(N.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:et.control,border:`1px solid ${b.menuBorder}`,background:`linear-gradient(to right, ${ct}, ${b.menuBg}, ${at})`});const a=l.ownerDocument.createElement("span");a.textContent="+1",k.append(T,N,a),m.appendChild(k),_.appendChild(m);const M=l.ownerDocument.createElement("span");if(Object.assign(M.style,{color:b.value,fontSize:z.body,fontWeight:"500"}),e===0&&o===0)M.textContent=r?`No significant correlations found \u2014 strongest pair: ${r.xLabel} \u2194 ${r.yLabel} (r = ${r.r.toFixed(2)})`:"No significant correlations found";else{const w=[];e>0&&w.push(`${e} strong (|r| \u2265 0.7)`),o>0&&w.push(`${o} moderate (0.4\u20130.7)`);const F=e+o;let X=w.join(", ")+` pair${F!==1?"s":""} found`;u>0&&(X+=` \xB7 ${u} weak pair${u!==1?"s":""} not shown`),M.textContent=X}_.appendChild(M)}function gt(e){if(D.innerHTML="",e.tests.length<2)return kt(D,"Need at least two parametric tests for a correlation matrix."),()=>{};const o=l.ownerDocument.createElement("canvas");o.style.display="block",o.style.cursor="default",D.appendChild(o);const u=e.tests.length,r=s=>s.label.split(" (#")[0],p=Math.min(14,Math.max(...e.tests.map(s=>r(s).length))),i=p*6.5+8,m=10,g=Math.round(m*6.5*Math.sin(Math.PI/4))+14,k=14,T=28,N=44;let a=-1,M=-1;function w(s){const c=Math.max(0,s-i);return Math.max(k,Math.min(N,Math.floor(c/u)))}const F=new Map;for(const s of e.cells){let c=F.get(s.yIndex);c||(c=[],F.set(s.yIndex,c)),c.push(s)}function X(){const s=At(l),c=nt(s.bg)??[255,255,255],h=D.clientWidth,n=w(h),W=n*u,S=g+n*u+4,$=i+W,E=Bt(o,l,$,S);if(!E)return;const{ctx:t}=E;t.font=`${dt(-1)}px system-ui, sans-serif`,t.fillStyle=s.text,e.tests.forEach((R,y)=>{const v=r(R),A=v.length>m?`${v.slice(0,m-1)}\u2026`:v,Q=i+y*n+n/2;t.save(),t.translate(Q,g-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=y===a?s.text:s.textMuted,t.fillText(A,0,0),t.restore()}),e.tests.forEach((R,y)=>{const v=r(R),A=g+y*n,Q=A+n/2;t.font=`${dt(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle",t.fillStyle=y===M?s.text:s.textMuted,t.fillText(v.length>p?`${v.slice(0,p-1)}\u2026`:v,i-4,Q),(F.get(y)??[]).forEach(lt=>{const Z=lt.xIndex,P=i+Z*n,yt=Z===a&&y===M,rt=Z===y,V=lt.r;if(V===null){t.fillStyle=s.border,t.globalAlpha=.4,t.fillRect(P+1,A+1,n-2,n-2),t.globalAlpha=1;return}let tt=s.bgHover;rt?t.fillStyle=s.bgHover:(tt=_t(V>=0?at:ct,c,Math.abs(V)),t.fillStyle=tt),t.fillRect(P+1,A+1,n-2,n-2),n>=T&&!rt&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Ot(tt),t.textAlign="center",t.textBaseline="middle",t.fillText(V.toFixed(2),P+n/2,A+n/2)),yt&&(t.strokeStyle=s.text,t.lineWidth=2,t.strokeRect(P+1,A+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=s.bg,t.lineWidth=1;for(let R=0;R<=u;R++){const y=i+R*n;t.beginPath(),t.moveTo(y,g),t.lineTo(y,g+u*n),t.stroke();const v=g+R*n;t.beginPath(),t.moveTo(i,v),t.lineTo(i+u*n,v),t.stroke()}}function st(s){const c=o.getBoundingClientRect(),h=D.clientWidth,n=w(h),W=(s.clientX-c.left)*(o.width/ut(o)/c.width),S=(s.clientY-c.top)*(o.height/ut(o)/c.height),$=Math.floor((W-i)/n),E=Math.floor((S-g)/n);return $>=0&&$<u&&E>=0&&E<u?{xi:$,yi:E}:null}return o.addEventListener("mousemove",s=>{const c=st(s);if(!c){I.style.display="none",o.style.cursor="default";return}const{xi:h,yi:n}=c,W=h===n;o.style.cursor=W||!O?"default":"pointer";const S=e.cells.find(t=>t.xIndex===h&&t.yIndex===n),$=e.tests[h].label,E=e.tests[n].label;W?I.innerHTML=`<strong>${G($)}</strong>`:S?.r!==null&&S?.r!==void 0?I.innerHTML=`<strong>${G(r(e.tests[n]))}</strong> (#${e.tests[n].testNumber}) vs <strong>${G(r(e.tests[h]))}</strong> (#${e.tests[h].testNumber})<br>r = ${S.r.toFixed(4)} \xB7 n = ${S.n.toLocaleString()}${O?"<br><em>click to view in scatter</em>":""}`:I.innerHTML=`${G(`${E} vs ${$}`)}<br><em>insufficient data</em>`,I.style.display="block",Rt(I,l,s.clientX,s.clientY)}),o.addEventListener("mouseleave",()=>{I.style.display="none"}),o.addEventListener("click",s=>{const c=st(s);if(!c||!O)return;const{xi:h,yi:n}=c;h!==n&&(a=h,M=n,X(),O(e.tests[h].testNumber,e.tests[n].testNumber))}),X}let ot=null;function K(){const e=ht(),o=e.flatMap(a=>a.dies??[]),u=Mt(o,B),{matrix:r,strongPairs:p,moderatePairs:i,hiddenWeakPairs:m,strongestPair:g}=wt(u,{minTests:6,maxTests:Y}),k=vt(e,{facetableOnly:!0}).filter(a=>a.splittable).map(a=>a.label),T=r.cells.filter(a=>a.xIndex!==a.yIndex).map(a=>a.n).sort((a,M)=>a-M),N=T.length?T[Math.floor(T.length/2)]:null;ot=r,xt(p,i,m,g,k,N),J=gt(r),J()}const bt=$t(l,()=>J());return K(),{card:l,destroy:()=>bt.disconnect()}}
@@ -33,6 +33,7 @@ export interface GroupedBarPlotOptions {
33
33
  /** Sub-bar colour by group index. Defaults to the CVD-safe categorical ramp. */
34
34
  colorOf?: (groupIndex: number) => string;
35
35
  /** Tooltip HTML for a hovered sub-bar. */
36
+ /** HTML for the hover tooltip. Escape every label with `escHtml` (core/utils.ts): they come from input files. */
36
37
  tooltipHtml: (rowIndex: number, groupIndex: number) => string;
37
38
  }
38
39
  export interface GroupedBarPlotHandle {
@@ -1 +1 @@
1
- import{buildTestHistogramData as jt,collectTestValues as Ht,buildTestHistogramSeries as _t,testValueExtent as At}from"../../stats/histogram.js";import{SPACE as at,fontPx as Q,FONT as Ct,CLR as $t}from"../toolbar.js";import{fmt as bt}from"../../renderer/fmt.js";import{QUANTITY as Ut,categorical as Xt}from"./palette.js";import{cardShell as Yt,observeResize as Pt,makeTooltip as Qt,attachChartTip as qt,positionChartTooltip as Wt,makeLinkedTestSelect as Jt,makeWaferSelect as Zt,makeLinkedAxisPrefs as te,renderEmptyState as Tt,chartFillHeight as Dt,applyCanvasFlow as Et,makeAxisFormat as It,PADDING as ft,robustFence as Bt,shouldIncludeLimitsByDefault as Ft,drawOffAxisLimits as ee,resolveAxisRange as ne,chartSwatchCss as ie,makeSeriesLegendItem as oe,prepareCanvas as Kt}from"./chartShell.js";const St=230,Ot=36,yt=18;function Rt(l,G,q,J,Mt,B,r,y=4){const Z=Math.max(1,B/y),rt=Math.pow(10,Math.floor(Math.log10(Z))),d=Z/rt,tt=(d<=1?1:d<=2?2:d<=5?5:10)*rt,K=J-q;l.save(),l.font=`${Q(-1)}px system-ui, sans-serif`,l.textAlign="right",l.textBaseline="middle";for(let V=0;V<=B+1e-9;V+=tt){const ct=J-V/(B||1)*K;l.strokeStyle=r.grid,l.lineWidth=1,l.beginPath(),l.moveTo(G,ct),l.lineTo(G+Mt,ct),l.stroke(),l.fillStyle=r.text,l.fillText(String(Math.round(V)),G-4,ct)}l.strokeStyle=r.axis,l.lineWidth=1,l.beginPath(),l.moveTo(G,q),l.lineTo(G,J),l.stroke(),l.restore()}export function renderHistogramPanel(l){const{title:G="Test value distribution",items:q,testDefs:J,onSaveImage:Mt,groups:B}=l,{card:r,body:y,controlsRow:Z}=Yt(G,Mt,l.ownerDocument),rt=J.filter(s=>s.testNumber!==void 0);let d=l.selectedTestNumber??rt[0]?.testNumber??null,tt=null,K=0;const V=Jt(rt,d,s=>{d=s,j(),l.onTestChange?.(s)},{maxWidth:"200px",ownerDocument:r.ownerDocument}),ct=V.el;Z.appendChild(ct);const wt=Zt(q,tt,s=>{tt=s,j()},{ownerDocument:r.ownerDocument});Z.appendChild(wt);function Nt(s){return S===s||s!==null&&!(B??[]).some(v=>v.key===s)?!1:(S=s,j(),!0)}const Lt=r.ownerDocument.createElement("span");Object.assign(Lt.style,{display:"inline-flex",gap:at.lg,alignItems:"center"}),Z.appendChild(Lt);const gt=te(Lt,l.axisPrefs,s=>{l.onAxisPrefsChange?.(s),j()},r.ownerDocument),C=Qt(r);let dt=null,S=null;function et(s){const v=J.find(nt=>nt.testNumber===s);return{unit:v?.unit,limitLow:v?.limitLow,limitHigh:v?.limitHigh}}function j(){const{includeLimits:s,clipOutliers:v}=gt.get();if(dt?.disconnect(),dt=null,y.innerHTML="",d===null){Tt(y,"No parametric test data available for a histogram.");return}const nt=!!B&&B.length>0;let it=!1,u;if(nt){const{limitLow:a,limitHigh:m}=et(d),o=B.flatMap(x=>x.items),{min:e,max:p}=At(o,d);if(it=s??Ft(e,p,a,m),v){const x=Ht(o,d),H=Bt(x);H&&(u={lo:Math.max(e,H.lo),hi:Math.min(p,H.hi)},K=x.reduce((lt,E)=>lt+(E<u.lo||E>u.hi?1:0),0))}u||(K=0)}const $=nt?_t(B,d,16,it?et(d).limitLow:void 0,it?et(d).limitHigh:void 0,u):null;if(wt.style.display=$?"none":"",$){const{limitLow:a,limitHigh:m}=et(d);if(gt.sync(it,a!==void 0||m!==void 0),$.series.length===0){Tt(y,"No parametric test data available for a histogram.");return}zt($);return}const _=tt!==null?[q[tt]]:q,{unit:O,limitLow:P,limitHigh:W}=et(d),k=Ht(_,d),{min:ot,max:mt}=At(_,d),ut=s??Ft(ot,mt,P,W);gt.sync(ut,P!==void 0||W!==void 0);const U=v?Bt(k):null,X=U?{lo:Math.max(ot,U.lo),hi:Math.min(mt,U.hi)}:void 0;K=X?k.filter(a=>a<X.lo||a>X.hi).length:0;const h=jt(_,d,16,ut?P:void 0,ut?W:void 0,X);if(h.length===0){Tt(y,"No parametric test data available for a histogram.");return}const D=Math.max(...h.map(a=>a.count),1),Y=r.ownerDocument.createElement("div");Object.assign(Y.style,{fontSize:Ct.body,color:$t.label,marginBottom:at.xxs}),Y.textContent=`max ${D} dies/bucket`+(K?` \xB7 ${K} value${K===1?"":"s"} outside clipped range`:""),y.appendChild(Y);const M=r.ownerDocument.createElement("canvas");M.style.display="block",M.style.cursor="default",y.appendChild(M);let F=-1;const n=h[0].rangeLow,g=h[h.length-1].rangeHigh,L=g-n||1,t=It(Math.max(Math.abs(n),Math.abs(g)),O);function c(a){const m=ft+36,o=M.clientWidth-m-ft,e=a-Ot-yt;return{plotX:m,plotMaxWidth:Math.max(10,o),plotMaxHeight:e,plotTop:yt}}function f(a){const{plotX:m,plotMaxWidth:o}=c(St);if(a<m||a>m+o)return-1;const e=Math.floor((a-m)/o*h.length);return e>=0&&e<h.length?e:-1}function T(){Et(M,Y);const a=y.clientWidth,m=Dt(r,y,M,St),o=Kt(M,r,a,m);if(!o)return;const{ctx:e,theme:p}=o,{plotX:x,plotMaxWidth:H,plotMaxHeight:lt,plotTop:E}=c(m),i=E+lt,w=H/h.length;Rt(e,x,E,i,H,D,{text:p.textMuted,axis:p.border,grid:p.border}),h.forEach((b,z)=>{const I=b.count/D*lt,vt=x+z*w,kt=i-I;e.fillStyle=z===F?p.text:Ut,e.globalAlpha=z===F?1:.7,e.fillRect(vt+1,kt,Math.max(1,w-2),I),e.globalAlpha=1,z===F&&(e.fillStyle=p.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${b.count}`,vt+w/2,kt-2))});const A=b=>x+(b-n)/L*H;e.font=`${Q(-1)}px system-ui, sans-serif`;const{offAxis:ht}=ne({dataMin:n,dataMax:g,limitLow:P,limitHigh:W,includeLimits:!1});ee(e,ht,{left:x,right:x+H,top:E,bottom:i},"horizontal",p.limitLine,t.tick);const pt=new Set(ht.map(b=>b.value));for(const[b,z]of[[P,"LSL"],[W,"USL"]]){if(b===void 0||pt.has(b))continue;const I=A(b);e.strokeStyle=p.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(I,E),e.lineTo(I,i),e.stroke(),e.setLineDash([]),e.fillStyle=p.limitLine,e.textAlign="center",e.textBaseline="bottom",e.fillText(z,I,E-2)}e.font=`${Q(-1)}px system-ui, sans-serif`,e.strokeStyle=p.border,e.lineWidth=1,e.beginPath(),e.moveTo(x,i),e.lineTo(x+H,i),e.stroke(),e.fillStyle=p.textMuted,e.textAlign="center",e.textBaseline="top";const st=Math.max(2,Math.floor(H/44)),R=Math.ceil(h.length/st),N=Math.max(1,R);for(let b=0;b<=h.length;b+=N){const z=b<h.length?h[b].rangeLow:h[h.length-1].rangeHigh,I=x+b*w;e.beginPath(),e.moveTo(I,i),e.lineTo(I,i+4),e.stroke(),e.fillText(t.tick(z),I,i+6)}t.unitLabel&&(e.save(),e.font=`${Q(-1)}px system-ui, sans-serif`,e.fillStyle=p.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${t.unitLabel})`,x+H,i+20),e.restore())}M.addEventListener("mousemove",a=>{const m=M.getBoundingClientRect(),o=f(a.clientX-m.left);if(o!==F&&(F=o,T()),o>=0){const e=h[o];C.innerHTML=`<strong>${bt(e.rangeLow,O,"engineering")} \u2013 ${bt(e.rangeHigh,O,"engineering")}</strong><br>${e.count} dies`,C.style.display="block",Wt(C,r,a.clientX,a.clientY)}else C.style.display="none"}),M.addEventListener("mouseleave",()=>{F!==-1&&(F=-1,T()),C.style.display="none"}),dt=Pt(r,()=>T()),T()}function zt(s){const{unit:v,limitLow:nt,limitHigh:it}=et(d),u=s.ranges,$=s.series,_=Xt;S&&!$.some(n=>n.groupKey===S)&&(S=null);const O=Math.max(1,...$.flatMap(n=>n.counts)),P=r.ownerDocument.createElement("div");Object.assign(P.style,{fontSize:Ct.body,color:$t.label,marginBottom:at.xxs}),P.textContent=`${$.length} groups \xB7 max ${O} dies/bucket`,y.appendChild(P);const W=r.ownerDocument.createElement("div");Object.assign(W.style,{display:"flex",flexWrap:"wrap",gap:at.sm,marginTop:at.md,marginBottom:at.xl}),$.forEach((n,g)=>{const L=oe(n.groupKey,_(g),r.ownerDocument),t=L.el;qt(t,r,C,`${n.groupKey} \u2014 click to emphasize (dim the rest)`),L.setState({selected:S===n.groupKey,dimmed:S!==null&&S!==n.groupKey}),t.addEventListener("click",()=>{const c=S===n.groupKey?null:n.groupKey;Nt(c),l.onGroupChange?.(c)}),W.appendChild(t)}),y.appendChild(W);const k=r.ownerDocument.createElement("canvas");k.style.display="block",k.style.cursor="default",y.appendChild(k);const ot=u[0].rangeLow,mt=u[u.length-1].rangeHigh,ut=mt-ot||1,U=It(Math.max(Math.abs(ot),Math.abs(mt)),v),X=n=>{const g=(n.ownerDocument.defaultView??window).getComputedStyle(n);return n.offsetHeight+(parseFloat(g.marginTop)||0)+(parseFloat(g.marginBottom)||0)},h=()=>X(P)+X(W);let D=-1,Y={plotX:ft+36,plotMaxWidth:10,barWidth:10};function M(){Et(k,h());const n=y.clientWidth,g=Dt(r,y,k,St),L=Kt(k,r,n,g);if(!L)return;const{ctx:t,theme:c}=L,f=ft+36,T=Math.max(10,n-f-ft),a=yt,m=g-Ot-yt,o=a+m,e=T/u.length;Y={plotX:f,plotMaxWidth:T,barWidth:e},Rt(t,f,a,o,T,O,{text:c.textMuted,axis:c.border,grid:c.border}),D>=0&&(t.fillStyle=c.bgHover,t.globalAlpha=.5,t.fillRect(f+D*e,a,e,m),t.globalAlpha=1);const p=$.map((i,w)=>({s:i,i:w})).sort((i,w)=>(i.s.groupKey===S?1:0)-(w.s.groupKey===S?1:0));for(const{s:i,i:w}of p){const A=S===i.groupKey,ht=S!==null&&!A,pt=_(w);t.strokeStyle=pt,t.fillStyle=pt,t.lineWidth=A?2:1,t.globalAlpha=ht?.12:S?.32:.22,t.beginPath(),t.moveTo(f,o),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/O*m;t.lineTo(R,N),t.lineTo(R+e,N)}),t.lineTo(f+T,o),t.closePath(),t.fill(),t.globalAlpha=ht?.3:1,t.beginPath(),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/O*m;st===0?t.moveTo(R,N):t.lineTo(R,N),t.lineTo(R+e,N)}),t.stroke()}t.globalAlpha=1;const x=i=>f+(i-ot)/ut*T;t.font=`${Q(-1)}px system-ui, sans-serif`;for(const[i,w]of[[nt,"LSL"],[it,"USL"]]){if(i===void 0)continue;const A=x(i);t.strokeStyle=c.limitLine,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(A,a),t.lineTo(A,o),t.stroke(),t.setLineDash([]),t.fillStyle=c.limitLine,t.textAlign="center",t.textBaseline="bottom",t.fillText(w,A,a-2)}t.font=`${Q(-1)}px system-ui, sans-serif`,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.moveTo(f,o),t.lineTo(f+T,o),t.stroke(),t.fillStyle=c.textMuted,t.textAlign="center",t.textBaseline="top";const lt=Math.max(2,Math.floor(T/44)),E=Math.max(1,Math.ceil(u.length/lt));for(let i=0;i<=u.length;i+=E){const w=i<u.length?u[i].rangeLow:u[u.length-1].rangeHigh,A=f+i*e;t.beginPath(),t.moveTo(A,o),t.lineTo(A,o+4),t.stroke(),t.fillText(U.tick(w),A,o+6)}U.unitLabel&&(t.save(),t.font=`${Q(-1)}px system-ui, sans-serif`,t.fillStyle=c.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${U.unitLabel})`,f+T,o+20),t.restore())}function F(n){const{plotX:g,plotMaxWidth:L,barWidth:t}=Y;if(n<g||n>g+L)return-1;const c=Math.floor((n-g)/t);return c>=0&&c<u.length?c:-1}k.addEventListener("mousemove",n=>{const g=k.getBoundingClientRect(),L=F(n.clientX-g.left);if(L!==D&&(D=L,M()),L>=0){const t=u[L],c=$.map((f,T)=>`<span style="${ie(_(T))};margin-right:4px"></span>${f.groupKey}: ${f.counts[L]}`).join("<br>");C.innerHTML=`<strong>${bt(t.rangeLow,v,"engineering")} \u2013 ${bt(t.rangeHigh,v,"engineering")}</strong><br>${c}`,C.style.display="block",Wt(C,r,n.clientX,n.clientY)}else C.style.display="none"}),k.addEventListener("mouseleave",()=>{D!==-1&&(D=-1,M()),C.style.display="none"}),dt=Pt(r,()=>M()),M()}j();function Gt(s){V.set(s)&&(d=s,j())}function Vt(s){gt.set(s)&&j()}return{card:r,setTest:Gt,setAxisPrefs:Vt,destroy:()=>dt?.disconnect()}}
1
+ import{buildTestHistogramData as _t,collectTestValues as At,buildTestHistogramSeries as Ut,testValueExtent as Ct}from"../../stats/histogram.js";import{SPACE as at,fontPx as Q,FONT as $t,CLR as Pt}from"../toolbar.js";import{fmt as bt}from"../../renderer/fmt.js";import{QUANTITY as Xt,categorical as Yt}from"./palette.js";import{cardShell as Qt,observeResize as Wt,makeTooltip as qt,attachChartTip as Jt,positionChartTooltip as Dt,makeLinkedTestSelect as Zt,makeWaferSelect as te,makeLinkedAxisPrefs as ee,renderEmptyState as Tt,chartFillHeight as Et,applyCanvasFlow as It,makeAxisFormat as Bt,PADDING as ft,robustFence as Ft,shouldIncludeLimitsByDefault as Kt,drawOffAxisLimits as ne,resolveAxisRange as ie,chartSwatchCss as oe,makeSeriesLegendItem as le,prepareCanvas as Ot}from"./chartShell.js";import{escHtml as St}from"../../core/utils.js";const wt=230,Rt=36,yt=18;function Nt(l,G,q,J,Mt,B,r,y=4){const Z=Math.max(1,B/y),rt=Math.pow(10,Math.floor(Math.log10(Z))),d=Z/rt,tt=(d<=1?1:d<=2?2:d<=5?5:10)*rt,K=J-q;l.save(),l.font=`${Q(-1)}px system-ui, sans-serif`,l.textAlign="right",l.textBaseline="middle";for(let V=0;V<=B+1e-9;V+=tt){const ct=J-V/(B||1)*K;l.strokeStyle=r.grid,l.lineWidth=1,l.beginPath(),l.moveTo(G,ct),l.lineTo(G+Mt,ct),l.stroke(),l.fillStyle=r.text,l.fillText(String(Math.round(V)),G-4,ct)}l.strokeStyle=r.axis,l.lineWidth=1,l.beginPath(),l.moveTo(G,q),l.lineTo(G,J),l.stroke(),l.restore()}export function renderHistogramPanel(l){const{title:G="Test value distribution",items:q,testDefs:J,onSaveImage:Mt,groups:B}=l,{card:r,body:y,controlsRow:Z}=Qt(G,Mt,l.ownerDocument),rt=J.filter(s=>s.testNumber!==void 0);let d=l.selectedTestNumber??rt[0]?.testNumber??null,tt=null,K=0;const V=Zt(rt,d,s=>{d=s,j(),l.onTestChange?.(s)},{maxWidth:"200px",ownerDocument:r.ownerDocument}),ct=V.el;Z.appendChild(ct);const vt=te(q,tt,s=>{tt=s,j()},{ownerDocument:r.ownerDocument});Z.appendChild(vt);function zt(s){return S===s||s!==null&&!(B??[]).some(v=>v.key===s)?!1:(S=s,j(),!0)}const Lt=r.ownerDocument.createElement("span");Object.assign(Lt.style,{display:"inline-flex",gap:at.lg,alignItems:"center"}),Z.appendChild(Lt);const gt=ee(Lt,l.axisPrefs,s=>{l.onAxisPrefsChange?.(s),j()},r.ownerDocument),C=qt(r);let dt=null,S=null;function et(s){const v=J.find(nt=>nt.testNumber===s);return{unit:v?.unit,limitLow:v?.limitLow,limitHigh:v?.limitHigh}}function j(){const{includeLimits:s,clipOutliers:v}=gt.get();if(dt?.disconnect(),dt=null,y.innerHTML="",d===null){Tt(y,"No parametric test data available for a histogram.");return}const nt=!!B&&B.length>0;let it=!1,u;if(nt){const{limitLow:a,limitHigh:m}=et(d),o=B.flatMap(x=>x.items),{min:e,max:p}=Ct(o,d);if(it=s??Kt(e,p,a,m),v){const x=At(o,d),H=Ft(x);H&&(u={lo:Math.max(e,H.lo),hi:Math.min(p,H.hi)},K=x.reduce((lt,E)=>lt+(E<u.lo||E>u.hi?1:0),0))}u||(K=0)}const $=nt?Ut(B,d,16,it?et(d).limitLow:void 0,it?et(d).limitHigh:void 0,u):null;if(vt.style.display=$?"none":"",$){const{limitLow:a,limitHigh:m}=et(d);if(gt.sync(it,a!==void 0||m!==void 0),$.series.length===0){Tt(y,"No parametric test data available for a histogram.");return}Gt($);return}const _=tt!==null?[q[tt]]:q,{unit:O,limitLow:P,limitHigh:W}=et(d),k=At(_,d),{min:ot,max:mt}=Ct(_,d),ut=s??Kt(ot,mt,P,W);gt.sync(ut,P!==void 0||W!==void 0);const U=v?Ft(k):null,X=U?{lo:Math.max(ot,U.lo),hi:Math.min(mt,U.hi)}:void 0;K=X?k.filter(a=>a<X.lo||a>X.hi).length:0;const h=_t(_,d,16,ut?P:void 0,ut?W:void 0,X);if(h.length===0){Tt(y,"No parametric test data available for a histogram.");return}const D=Math.max(...h.map(a=>a.count),1),Y=r.ownerDocument.createElement("div");Object.assign(Y.style,{fontSize:$t.body,color:Pt.label,marginBottom:at.xxs}),Y.textContent=`max ${D} dies/bucket`+(K?` \xB7 ${K} value${K===1?"":"s"} outside clipped range`:""),y.appendChild(Y);const M=r.ownerDocument.createElement("canvas");M.style.display="block",M.style.cursor="default",y.appendChild(M);let F=-1;const n=h[0].rangeLow,g=h[h.length-1].rangeHigh,L=g-n||1,t=Bt(Math.max(Math.abs(n),Math.abs(g)),O);function c(a){const m=ft+36,o=M.clientWidth-m-ft,e=a-Rt-yt;return{plotX:m,plotMaxWidth:Math.max(10,o),plotMaxHeight:e,plotTop:yt}}function f(a){const{plotX:m,plotMaxWidth:o}=c(wt);if(a<m||a>m+o)return-1;const e=Math.floor((a-m)/o*h.length);return e>=0&&e<h.length?e:-1}function T(){It(M,Y);const a=y.clientWidth,m=Et(r,y,M,wt),o=Ot(M,r,a,m);if(!o)return;const{ctx:e,theme:p}=o,{plotX:x,plotMaxWidth:H,plotMaxHeight:lt,plotTop:E}=c(m),i=E+lt,w=H/h.length;Nt(e,x,E,i,H,D,{text:p.textMuted,axis:p.border,grid:p.border}),h.forEach((b,z)=>{const I=b.count/D*lt,kt=x+z*w,Ht=i-I;e.fillStyle=z===F?p.text:Xt,e.globalAlpha=z===F?1:.7,e.fillRect(kt+1,Ht,Math.max(1,w-2),I),e.globalAlpha=1,z===F&&(e.fillStyle=p.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${b.count}`,kt+w/2,Ht-2))});const A=b=>x+(b-n)/L*H;e.font=`${Q(-1)}px system-ui, sans-serif`;const{offAxis:ht}=ie({dataMin:n,dataMax:g,limitLow:P,limitHigh:W,includeLimits:!1});ne(e,ht,{left:x,right:x+H,top:E,bottom:i},"horizontal",p.limitLine,t.tick);const pt=new Set(ht.map(b=>b.value));for(const[b,z]of[[P,"LSL"],[W,"USL"]]){if(b===void 0||pt.has(b))continue;const I=A(b);e.strokeStyle=p.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(I,E),e.lineTo(I,i),e.stroke(),e.setLineDash([]),e.fillStyle=p.limitLine,e.textAlign="center",e.textBaseline="bottom",e.fillText(z,I,E-2)}e.font=`${Q(-1)}px system-ui, sans-serif`,e.strokeStyle=p.border,e.lineWidth=1,e.beginPath(),e.moveTo(x,i),e.lineTo(x+H,i),e.stroke(),e.fillStyle=p.textMuted,e.textAlign="center",e.textBaseline="top";const st=Math.max(2,Math.floor(H/44)),R=Math.ceil(h.length/st),N=Math.max(1,R);for(let b=0;b<=h.length;b+=N){const z=b<h.length?h[b].rangeLow:h[h.length-1].rangeHigh,I=x+b*w;e.beginPath(),e.moveTo(I,i),e.lineTo(I,i+4),e.stroke(),e.fillText(t.tick(z),I,i+6)}t.unitLabel&&(e.save(),e.font=`${Q(-1)}px system-ui, sans-serif`,e.fillStyle=p.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${t.unitLabel})`,x+H,i+20),e.restore())}M.addEventListener("mousemove",a=>{const m=M.getBoundingClientRect(),o=f(a.clientX-m.left);if(o!==F&&(F=o,T()),o>=0){const e=h[o];C.innerHTML=`<strong>${St(`${bt(e.rangeLow,O,"engineering")} \u2013 ${bt(e.rangeHigh,O,"engineering")}`)}</strong><br>${e.count} dies`,C.style.display="block",Dt(C,r,a.clientX,a.clientY)}else C.style.display="none"}),M.addEventListener("mouseleave",()=>{F!==-1&&(F=-1,T()),C.style.display="none"}),dt=Wt(r,()=>T()),T()}function Gt(s){const{unit:v,limitLow:nt,limitHigh:it}=et(d),u=s.ranges,$=s.series,_=Yt;S&&!$.some(n=>n.groupKey===S)&&(S=null);const O=Math.max(1,...$.flatMap(n=>n.counts)),P=r.ownerDocument.createElement("div");Object.assign(P.style,{fontSize:$t.body,color:Pt.label,marginBottom:at.xxs}),P.textContent=`${$.length} groups \xB7 max ${O} dies/bucket`,y.appendChild(P);const W=r.ownerDocument.createElement("div");Object.assign(W.style,{display:"flex",flexWrap:"wrap",gap:at.sm,marginTop:at.md,marginBottom:at.xl}),$.forEach((n,g)=>{const L=le(n.groupKey,_(g),r.ownerDocument),t=L.el;Jt(t,r,C,`${n.groupKey} \u2014 click to emphasize (dim the rest)`),L.setState({selected:S===n.groupKey,dimmed:S!==null&&S!==n.groupKey}),t.addEventListener("click",()=>{const c=S===n.groupKey?null:n.groupKey;zt(c),l.onGroupChange?.(c)}),W.appendChild(t)}),y.appendChild(W);const k=r.ownerDocument.createElement("canvas");k.style.display="block",k.style.cursor="default",y.appendChild(k);const ot=u[0].rangeLow,mt=u[u.length-1].rangeHigh,ut=mt-ot||1,U=Bt(Math.max(Math.abs(ot),Math.abs(mt)),v),X=n=>{const g=(n.ownerDocument.defaultView??window).getComputedStyle(n);return n.offsetHeight+(parseFloat(g.marginTop)||0)+(parseFloat(g.marginBottom)||0)},h=()=>X(P)+X(W);let D=-1,Y={plotX:ft+36,plotMaxWidth:10,barWidth:10};function M(){It(k,h());const n=y.clientWidth,g=Et(r,y,k,wt),L=Ot(k,r,n,g);if(!L)return;const{ctx:t,theme:c}=L,f=ft+36,T=Math.max(10,n-f-ft),a=yt,m=g-Rt-yt,o=a+m,e=T/u.length;Y={plotX:f,plotMaxWidth:T,barWidth:e},Nt(t,f,a,o,T,O,{text:c.textMuted,axis:c.border,grid:c.border}),D>=0&&(t.fillStyle=c.bgHover,t.globalAlpha=.5,t.fillRect(f+D*e,a,e,m),t.globalAlpha=1);const p=$.map((i,w)=>({s:i,i:w})).sort((i,w)=>(i.s.groupKey===S?1:0)-(w.s.groupKey===S?1:0));for(const{s:i,i:w}of p){const A=S===i.groupKey,ht=S!==null&&!A,pt=_(w);t.strokeStyle=pt,t.fillStyle=pt,t.lineWidth=A?2:1,t.globalAlpha=ht?.12:S?.32:.22,t.beginPath(),t.moveTo(f,o),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/O*m;t.lineTo(R,N),t.lineTo(R+e,N)}),t.lineTo(f+T,o),t.closePath(),t.fill(),t.globalAlpha=ht?.3:1,t.beginPath(),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/O*m;st===0?t.moveTo(R,N):t.lineTo(R,N),t.lineTo(R+e,N)}),t.stroke()}t.globalAlpha=1;const x=i=>f+(i-ot)/ut*T;t.font=`${Q(-1)}px system-ui, sans-serif`;for(const[i,w]of[[nt,"LSL"],[it,"USL"]]){if(i===void 0)continue;const A=x(i);t.strokeStyle=c.limitLine,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(A,a),t.lineTo(A,o),t.stroke(),t.setLineDash([]),t.fillStyle=c.limitLine,t.textAlign="center",t.textBaseline="bottom",t.fillText(w,A,a-2)}t.font=`${Q(-1)}px system-ui, sans-serif`,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.moveTo(f,o),t.lineTo(f+T,o),t.stroke(),t.fillStyle=c.textMuted,t.textAlign="center",t.textBaseline="top";const lt=Math.max(2,Math.floor(T/44)),E=Math.max(1,Math.ceil(u.length/lt));for(let i=0;i<=u.length;i+=E){const w=i<u.length?u[i].rangeLow:u[u.length-1].rangeHigh,A=f+i*e;t.beginPath(),t.moveTo(A,o),t.lineTo(A,o+4),t.stroke(),t.fillText(U.tick(w),A,o+6)}U.unitLabel&&(t.save(),t.font=`${Q(-1)}px system-ui, sans-serif`,t.fillStyle=c.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${U.unitLabel})`,f+T,o+20),t.restore())}function F(n){const{plotX:g,plotMaxWidth:L,barWidth:t}=Y;if(n<g||n>g+L)return-1;const c=Math.floor((n-g)/t);return c>=0&&c<u.length?c:-1}k.addEventListener("mousemove",n=>{const g=k.getBoundingClientRect(),L=F(n.clientX-g.left);if(L!==D&&(D=L,M()),L>=0){const t=u[L],c=$.map((f,T)=>`<span style="${oe(_(T))};margin-right:4px"></span>${St(`${f.groupKey}: ${f.counts[L]}`)}`).join("<br>");C.innerHTML=`<strong>${St(`${bt(t.rangeLow,v,"engineering")} \u2013 ${bt(t.rangeHigh,v,"engineering")}`)}</strong><br>${c}`,C.style.display="block",Dt(C,r,n.clientX,n.clientY)}else C.style.display="none"}),k.addEventListener("mouseleave",()=>{D!==-1&&(D=-1,M()),C.style.display="none"}),dt=Wt(r,()=>M()),M()}j();function Vt(s){V.set(s)&&(d=s,j())}function jt(s){gt.set(s)&&j()}return{card:r,setTest:Vt,setAxisPrefs:jt,destroy:()=>dt?.disconnect()}}
@@ -1 +1 @@
1
- import{buildTestPassRateData as E,hasJudgeableTests as F}from"../../stats/testPassRate.js";import{categorical as B,QUANTITY as I}from"./palette.js";import{SPACE as N,FONT as _,CLR as T}from"../toolbar.js";import{cardShell as A,makeTooltip as H,makeSegmented as O,renderEmptyState as w}from"./chartShell.js";import{renderGroupedBarPlot as j}from"./groupedBarPlot.js";const M=100,k=8,G={spec:"Spec limits",testFlag:"Tester flag",functional:"Functional"},U={spec:"Parametric pass rate \xB7 spec limits",testFlag:"Parametric pass rate \xB7 tester flag",functional:"Functional test pass rate"};export function renderTestPassRatePanel(g){const{groups:f,testDefs:u,onSaveImage:y}=g,p=g.ownerDocument??document,$=f.flatMap(e=>e.items.flatMap(a=>a.dies??[])),d=["spec","testFlag","functional"].filter(e=>F(u,e,$));let s=d[0]??"spec";const b=e=>U[e],{card:o,heading:P,body:i,controlsRow:v}=A(b(s),y,p);o.style.alignSelf="start",d.length>1&&v.appendChild(O(d.map(e=>[e,G[e]]),s,e=>{s=e,P.textContent=b(s),D()},p));const n=p.createElement("div");Object.assign(n.style,{color:T.label,fontSize:_.body,marginBottom:N.sm}),o.insertBefore(n,i);const h=H(o);let m=null;function D(){if(m?.destroy(),m=null,i.innerHTML="",d.length===0){n.textContent="",w(i,"No tests with a pass/fail verdict \u2014 parametric tests need spec limits, functional tests need recorded results.");return}const e=E(f,u,s);if(e.rows.length===0){n.textContent="",w(i,s==="spec"?"No parametric test has spec limits to judge against.":s==="testFlag"?"No parametric test has a recorded tester verdict.":"No functional test has recorded results.");return}const a=e.groups.length>1,x=s==="spec"?"Dies within spec limits, per test":s==="testFlag"?"Dies the tester marked pass, per test":"Dies passing the recorded verdict, per test";if(n.textContent=x+(a?" \xB7 one bar per group":"")+" \xB7 worst first",e.disagreementDies){const t=p.createElement("span");t.textContent=` \xB7 ${e.disagreementDies.toLocaleString()} die${e.disagreementDies===1?"":"s"} judged differently by limits and tester flag`,Object.assign(t.style,{color:T.warnText}),n.appendChild(t)}const L=e.rows,R=t=>a?B(t):I;m=j(o,i,h,{rows:L.map(t=>({label:t.label,bars:t.byGroup.map(l=>({fraction:l.passRatePercent===null?null:l.passRatePercent/100})),trailing:t.overall.passRatePercent===null?"\u2014":`${t.overall.passRatePercent.toFixed(1)}%`})),groups:e.groups,labelWidth:M,maxVisibleRows:k,showLegend:a,maxLabelChars:14,emptyBarText:"no data",colorOf:R,tooltipHtml:(t,l)=>{const r=L[t],c=r.byGroup[l],S=a?` \xB7 ${e.groups[l]}`:"";if(c.passRatePercent===null)return`<strong>${r.label}</strong>${S}<br>no dies with a verdict`;const C=s==="spec"&&!a&&(r.failLowDies||r.failHighDies)?`<br>${r.failLowDies??0} below LSL \xB7 ${r.failHighDies??0} above USL`:"";return`<strong>${r.label}</strong>${S}<br>${c.passRatePercent.toFixed(1)}% pass<br>${c.passDies.toLocaleString()} pass \xB7 ${c.failDies.toLocaleString()} fail \xB7 n = ${c.totalDies.toLocaleString()}`+C}})}return D(),{card:o,destroy:()=>{m?.destroy(),h.remove(),o.remove()}}}
1
+ import{buildTestPassRateData as F,hasJudgeableTests as B}from"../../stats/testPassRate.js";import{categorical as I,QUANTITY as N}from"./palette.js";import{SPACE as H,FONT as _,CLR as w}from"../toolbar.js";import{cardShell as A,makeTooltip as O,makeSegmented as j,renderEmptyState as y}from"./chartShell.js";import{renderGroupedBarPlot as M}from"./groupedBarPlot.js";import{escHtml as g}from"../../core/utils.js";const k=100,G=8,U={spec:"Spec limits",testFlag:"Tester flag",functional:"Functional"},K={spec:"Parametric pass rate \xB7 spec limits",testFlag:"Parametric pass rate \xB7 tester flag",functional:"Functional test pass rate"};export function renderTestPassRatePanel(f){const{groups:u,testDefs:b,onSaveImage:$}=f,p=f.ownerDocument??document,P=u.flatMap(e=>e.items.flatMap(a=>a.dies??[])),d=["spec","testFlag","functional"].filter(e=>B(b,e,P));let s=d[0]??"spec";const h=e=>K[e],{card:o,heading:v,body:i,controlsRow:x}=A(h(s),$,p);o.style.alignSelf="start",d.length>1&&x.appendChild(j(d.map(e=>[e,U[e]]),s,e=>{s=e,v.textContent=h(s),L()},p));const n=p.createElement("div");Object.assign(n.style,{color:w.label,fontSize:_.body,marginBottom:H.sm}),o.insertBefore(n,i);const D=O(o);let m=null;function L(){if(m?.destroy(),m=null,i.innerHTML="",d.length===0){n.textContent="",y(i,"No tests with a pass/fail verdict \u2014 parametric tests need spec limits, functional tests need recorded results.");return}const e=F(u,b,s);if(e.rows.length===0){n.textContent="",y(i,s==="spec"?"No parametric test has spec limits to judge against.":s==="testFlag"?"No parametric test has a recorded tester verdict.":"No functional test has recorded results.");return}const a=e.groups.length>1,R=s==="spec"?"Dies within spec limits, per test":s==="testFlag"?"Dies the tester marked pass, per test":"Dies passing the recorded verdict, per test";if(n.textContent=R+(a?" \xB7 one bar per group":"")+" \xB7 worst first",e.disagreementDies){const t=p.createElement("span");t.textContent=` \xB7 ${e.disagreementDies.toLocaleString()} die${e.disagreementDies===1?"":"s"} judged differently by limits and tester flag`,Object.assign(t.style,{color:w.warnText}),n.appendChild(t)}const S=e.rows,C=t=>a?I(t):N;m=M(o,i,D,{rows:S.map(t=>({label:t.label,bars:t.byGroup.map(l=>({fraction:l.passRatePercent===null?null:l.passRatePercent/100})),trailing:t.overall.passRatePercent===null?"\u2014":`${t.overall.passRatePercent.toFixed(1)}%`})),groups:e.groups,labelWidth:k,maxVisibleRows:G,showLegend:a,maxLabelChars:14,emptyBarText:"no data",colorOf:C,tooltipHtml:(t,l)=>{const r=S[t],c=r.byGroup[l],T=a?` \xB7 ${g(String(e.groups[l]))}`:"";if(c.passRatePercent===null)return`<strong>${g(r.label)}</strong>${T}<br>no dies with a verdict`;const E=s==="spec"&&!a&&(r.failLowDies||r.failHighDies)?`<br>${r.failLowDies??0} below LSL \xB7 ${r.failHighDies??0} above USL`:"";return`<strong>${g(r.label)}</strong>${T}<br>${c.passRatePercent.toFixed(1)}% pass<br>${c.passDies.toLocaleString()} pass \xB7 ${c.failDies.toLocaleString()} fail \xB7 n = ${c.totalDies.toLocaleString()}`+E}})}return L(),{card:o,destroy:()=>{m?.destroy(),D.remove(),o.remove()}}}
@@ -1 +1 @@
1
- import{buildTestTrendData as de,trendCentre as fe}from"../../stats/trend.js";import{isParametricTest as ue}from"../../renderer/buildWaferMap.js";import{fmt as D}from"../../renderer/fmt.js";import{SPACE as ie,fontPx as he,FONT as pe,CLR as ge}from"../toolbar.js";import{QUANTITY as _}from"./palette.js";import{cardShell as xe,observeResize as be,makeTooltip as ye,positionChartTooltip as ve,makeLinkedTestSelect as Te,makeLinkedAxisPrefs as Le,renderEmptyState as z,growCardToFitContent as we,chartFillHeight as ke,PADDING as X,resolveAxisRange as Ce,shouldIncludeLimitsByDefault as Se,drawOffAxisLimits as Ae,prepareCanvas as Pe}from"./chartShell.js";const oe=220,Ne=56,De=34,Me=3.5;export function renderTrendPanel(g){const{items:j,testDefs:le,onSaveImage:se,onOpen:C,centreLabel:G="mean of all wafers"}=g,re=g.title??"Wafer-to-wafer trend",{card:l,body:f,controlsRow:U}=xe(re,se,g.ownerDocument);l.style.alignSelf="start";const M=le.filter(o=>o.testNumber!==void 0&&ue(o));let h=g.selectedTestNumber??M[0]?.testNumber??null,I=0;const Y=Te(M,h,o=>{h=o,S(),g.onTestChange?.(o)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:l.ownerDocument});U.appendChild(Y.el);const O=l.ownerDocument.createElement("span");Object.assign(O.style,{display:"inline-flex",gap:ie.lg,alignItems:"center"}),U.appendChild(O);const $=Le(O,g.axisPrefs,o=>{g.onAxisPrefsChange?.(o),S()},l.ownerDocument),T=l.ownerDocument.createElement("div");Object.assign(T.style,{color:ge.label,fontSize:pe.body,marginBottom:ie.sm}),l.insertBefore(T,f);const L=ye(l);let H=null,n=[];function ae(o){return M.find(B=>B.testNumber===o)}function S(){const{includeLimits:o,clipOutliers:B}=$.get();if(f.innerHTML="",H?.disconnect(),H=null,M.length===0||h===null){T.textContent="",z(f,"No parametric test data available for a trend.");return}if(j.length<2){T.textContent="",z(f,"A wafer-to-wafer trend needs at least two wafers.");return}if(n=de(j,h),!n.some(r=>r.count>0)){T.textContent="",z(f,"No values recorded for this test.");return}const s=ae(h),x=fe(n),Q=()=>{T.textContent=`Point = wafer mean, whisker = \xB11\u03C3, dashed = ${G} \xB7 wafers in slot order`+(C?" \xB7 click a point to open that wafer":"")+(I?` \xB7 axis clipped, ${I} outside`:"")};Q();const u=l.ownerDocument.createElement("canvas");u.style.display="block",f.appendChild(u);let A=null;const q=()=>{const r=Math.max(1,f.clientWidth),d=Math.max(oe,ke(l,f,u,oe)),i=Pe(u,l,r,d);if(!i)return;const{ctx:e,theme:b}=i;let P=1/0,N=-1/0;const K=[];for(const t of n)!t.count||!Number.isFinite(t.mean)||(P=Math.min(P,t.mean-t.stddev),N=Math.max(N,t.mean+t.stddev),K.push(t.mean-t.stddev,t.mean+t.stddev));if(!Number.isFinite(P)||!Number.isFinite(N))return;const V=o??Se(P,N,s?.limitLow,s?.limitHigh);$.sync(V,s?.limitLow!==void 0||s?.limitHigh!==void 0);const E=Ce({dataMin:P,dataMax:N,limitLow:s?.limitLow,limitHigh:s?.limitHigh,includeLimits:V,clipOutliers:B,values:K});I=E.clippedCount,Q();let p=E.lo,w=E.hi;const Z=(w-p)*.08;p-=Z,w+=Z;const a=X+Ne;e.font=`${he(-1)}px system-ui, sans-serif`;const ee=t=>t.length>8?`${t.slice(0,7)}\u2026`:t,ce=n.length?Math.ceil(Math.max(e.measureText(ee(n[0].label)).width,e.measureText(ee(n[n.length-1].label)).width)/2):0,y=r-X-ce,F=X,k=d-De,v=t=>k-(t-p)/(w-p)*(k-F),te=n.length>1?(y-a)/(n.length-1):0;A={left:a,step:te};const R=t=>n.length>1?a+t*te:(a+y)/2;e.strokeStyle=b.border,e.lineWidth=1,e.beginPath(),e.moveTo(a,F),e.lineTo(a,k),e.lineTo(y,k),e.stroke(),e.fillStyle=b.text,e.textAlign="right",e.textBaseline="middle";for(const t of[0,.5,1]){const c=p+(w-p)*t;e.fillText(D(c,s?.unit),a-6,v(c))}Ae(e,E.offAxis,{left:a,right:y,top:F,bottom:k},"vertical",b.limitLine,t=>D(t,s?.unit));for(const[t,c]of[[s?.limitLow,"LSL"],[s?.limitHigh,"USL"]]){if(t===void 0||t<p||t>w)continue;const m=v(t);e.save(),e.strokeStyle=b.limitLine,e.globalAlpha=.7,e.setLineDash([4,3]),e.beginPath(),e.moveTo(a,m),e.lineTo(y,m),e.stroke(),e.restore(),e.save(),e.fillStyle=b.limitLine,e.textAlign="left",e.fillText(c,a+3,m-6),e.restore()}if(x!==null&&x>=p&&x<=w){const t=v(x);e.save(),e.strokeStyle=b.text,e.globalAlpha=.5,e.setLineDash([3,3]),e.beginPath(),e.moveTo(a,t),e.lineTo(y,t),e.stroke(),e.restore()}e.strokeStyle=_,e.lineWidth=1.5,e.beginPath();let W=!1;n.forEach((t,c)=>{if(!t.count||!Number.isFinite(t.mean)){W=!1;return}const m=R(c),ne=v(t.mean);W?e.lineTo(m,ne):e.moveTo(m,ne),W=!0}),e.stroke(),n.forEach((t,c)=>{if(!t.count||!Number.isFinite(t.mean))return;const m=R(c);t.stddev>0&&(e.save(),e.strokeStyle=_,e.globalAlpha=.55,e.lineWidth=1,e.beginPath(),e.moveTo(m,v(t.mean-t.stddev)),e.lineTo(m,v(t.mean+t.stddev)),e.stroke(),e.restore()),e.fillStyle=_,e.beginPath(),e.arc(m,v(t.mean),Me,0,Math.PI*2),e.fill()}),e.fillStyle=b.text,e.textBaseline="top",e.textAlign="center";const me=Math.max(1,Math.ceil(n.length*42/Math.max(1,y-a)));n.forEach((t,c)=>{if(c%me!==0&&c!==n.length-1)return;const m=t.label.length>8?`${t.label.slice(0,7)}\u2026`:t.label;e.fillText(m,R(c),k+6)}),we(l,f,d)},J=r=>{if(!A||A.step<=0)return-1;const d=u.getBoundingClientRect(),i=Math.round((r.clientX-d.left-A.left)/A.step);return i>=0&&i<n.length&&n[i].count>0?i:-1};u.addEventListener("mousemove",r=>{const d=J(r);if(d<0){L.style.display="none",u.style.cursor="default";return}const i=n[d];u.style.cursor=C&&i.key!==void 0?"pointer":"default",L.innerHTML=`<strong>${i.label}</strong><br>mean ${D(i.mean,s?.unit)} \xB7 \u03C3 ${D(i.stddev,s?.unit)}<br>n = ${i.count.toLocaleString()}`+(x!==null?`<br>\u0394 vs ${G} ${i.mean-x>=0?"+":""}${D(i.mean-x,s?.unit)}`:"")+(C&&i.key!==void 0?"<br><em>click to open this wafer</em>":""),L.style.display="block",ve(L,l,r.clientX,r.clientY)}),u.addEventListener("mouseleave",()=>{L.style.display="none"}),u.addEventListener("click",r=>{const d=J(r);if(d<0||!C)return;const i=n[d];i.key!==void 0&&h!==null&&C(i.key,h)}),q(),H=be(f,q)}return S(),{card:l,destroy:()=>{H?.disconnect(),L.remove(),l.remove()},setTest:o=>{Y.set(o)&&(h=o,S())},setAxisPrefs:o=>{$.set(o)&&S()}}}
1
+ import{buildTestTrendData as fe,trendCentre as ue}from"../../stats/trend.js";import{isParametricTest as he}from"../../renderer/buildWaferMap.js";import{fmt as D}from"../../renderer/fmt.js";import{SPACE as oe,fontPx as pe,FONT as ge,CLR as xe}from"../toolbar.js";import{QUANTITY as _}from"./palette.js";import{cardShell as be,observeResize as ye,makeTooltip as ve,positionChartTooltip as Te,makeLinkedTestSelect as Le,makeLinkedAxisPrefs as we,renderEmptyState as z,growCardToFitContent as ke,chartFillHeight as Ce,PADDING as X,resolveAxisRange as Se,shouldIncludeLimitsByDefault as Ae,drawOffAxisLimits as Pe,prepareCanvas as Ne}from"./chartShell.js";import{escHtml as j}from"../../core/utils.js";const le=220,De=56,He=34,Me=3.5;export function renderTrendPanel(g){const{items:G,testDefs:se,onSaveImage:re,onOpen:C,centreLabel:U="mean of all wafers"}=g,ae=g.title??"Wafer-to-wafer trend",{card:l,body:f,controlsRow:Y}=be(ae,re,g.ownerDocument);l.style.alignSelf="start";const H=se.filter(o=>o.testNumber!==void 0&&he(o));let h=g.selectedTestNumber??H[0]?.testNumber??null,I=0;const Q=Le(H,h,o=>{h=o,S(),g.onTestChange?.(o)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:l.ownerDocument});Y.appendChild(Q.el);const O=l.ownerDocument.createElement("span");Object.assign(O.style,{display:"inline-flex",gap:oe.lg,alignItems:"center"}),Y.appendChild(O);const $=we(O,g.axisPrefs,o=>{g.onAxisPrefsChange?.(o),S()},l.ownerDocument),T=l.ownerDocument.createElement("div");Object.assign(T.style,{color:xe.label,fontSize:ge.body,marginBottom:oe.sm}),l.insertBefore(T,f);const L=ve(l);let M=null,n=[];function ce(o){return H.find(B=>B.testNumber===o)}function S(){const{includeLimits:o,clipOutliers:B}=$.get();if(f.innerHTML="",M?.disconnect(),M=null,H.length===0||h===null){T.textContent="",z(f,"No parametric test data available for a trend.");return}if(G.length<2){T.textContent="",z(f,"A wafer-to-wafer trend needs at least two wafers.");return}if(n=fe(G,h),!n.some(r=>r.count>0)){T.textContent="",z(f,"No values recorded for this test.");return}const s=ce(h),x=ue(n),q=()=>{T.textContent=`Point = wafer mean, whisker = \xB11\u03C3, dashed = ${U} \xB7 wafers in slot order`+(C?" \xB7 click a point to open that wafer":"")+(I?` \xB7 axis clipped, ${I} outside`:"")};q();const u=l.ownerDocument.createElement("canvas");u.style.display="block",f.appendChild(u);let A=null;const J=()=>{const r=Math.max(1,f.clientWidth),d=Math.max(le,Ce(l,f,u,le)),i=Ne(u,l,r,d);if(!i)return;const{ctx:e,theme:b}=i;let P=1/0,N=-1/0;const V=[];for(const t of n)!t.count||!Number.isFinite(t.mean)||(P=Math.min(P,t.mean-t.stddev),N=Math.max(N,t.mean+t.stddev),V.push(t.mean-t.stddev,t.mean+t.stddev));if(!Number.isFinite(P)||!Number.isFinite(N))return;const Z=o??Ae(P,N,s?.limitLow,s?.limitHigh);$.sync(Z,s?.limitLow!==void 0||s?.limitHigh!==void 0);const E=Se({dataMin:P,dataMax:N,limitLow:s?.limitLow,limitHigh:s?.limitHigh,includeLimits:Z,clipOutliers:B,values:V});I=E.clippedCount,q();let p=E.lo,w=E.hi;const ee=(w-p)*.08;p-=ee,w+=ee;const a=X+De;e.font=`${pe(-1)}px system-ui, sans-serif`;const te=t=>t.length>8?`${t.slice(0,7)}\u2026`:t,me=n.length?Math.ceil(Math.max(e.measureText(te(n[0].label)).width,e.measureText(te(n[n.length-1].label)).width)/2):0,y=r-X-me,F=X,k=d-He,v=t=>k-(t-p)/(w-p)*(k-F),ne=n.length>1?(y-a)/(n.length-1):0;A={left:a,step:ne};const R=t=>n.length>1?a+t*ne:(a+y)/2;e.strokeStyle=b.border,e.lineWidth=1,e.beginPath(),e.moveTo(a,F),e.lineTo(a,k),e.lineTo(y,k),e.stroke(),e.fillStyle=b.text,e.textAlign="right",e.textBaseline="middle";for(const t of[0,.5,1]){const c=p+(w-p)*t;e.fillText(D(c,s?.unit),a-6,v(c))}Pe(e,E.offAxis,{left:a,right:y,top:F,bottom:k},"vertical",b.limitLine,t=>D(t,s?.unit));for(const[t,c]of[[s?.limitLow,"LSL"],[s?.limitHigh,"USL"]]){if(t===void 0||t<p||t>w)continue;const m=v(t);e.save(),e.strokeStyle=b.limitLine,e.globalAlpha=.7,e.setLineDash([4,3]),e.beginPath(),e.moveTo(a,m),e.lineTo(y,m),e.stroke(),e.restore(),e.save(),e.fillStyle=b.limitLine,e.textAlign="left",e.fillText(c,a+3,m-6),e.restore()}if(x!==null&&x>=p&&x<=w){const t=v(x);e.save(),e.strokeStyle=b.text,e.globalAlpha=.5,e.setLineDash([3,3]),e.beginPath(),e.moveTo(a,t),e.lineTo(y,t),e.stroke(),e.restore()}e.strokeStyle=_,e.lineWidth=1.5,e.beginPath();let W=!1;n.forEach((t,c)=>{if(!t.count||!Number.isFinite(t.mean)){W=!1;return}const m=R(c),ie=v(t.mean);W?e.lineTo(m,ie):e.moveTo(m,ie),W=!0}),e.stroke(),n.forEach((t,c)=>{if(!t.count||!Number.isFinite(t.mean))return;const m=R(c);t.stddev>0&&(e.save(),e.strokeStyle=_,e.globalAlpha=.55,e.lineWidth=1,e.beginPath(),e.moveTo(m,v(t.mean-t.stddev)),e.lineTo(m,v(t.mean+t.stddev)),e.stroke(),e.restore()),e.fillStyle=_,e.beginPath(),e.arc(m,v(t.mean),Me,0,Math.PI*2),e.fill()}),e.fillStyle=b.text,e.textBaseline="top",e.textAlign="center";const de=Math.max(1,Math.ceil(n.length*42/Math.max(1,y-a)));n.forEach((t,c)=>{if(c%de!==0&&c!==n.length-1)return;const m=t.label.length>8?`${t.label.slice(0,7)}\u2026`:t.label;e.fillText(m,R(c),k+6)}),ke(l,f,d)},K=r=>{if(!A||A.step<=0)return-1;const d=u.getBoundingClientRect(),i=Math.round((r.clientX-d.left-A.left)/A.step);return i>=0&&i<n.length&&n[i].count>0?i:-1};u.addEventListener("mousemove",r=>{const d=K(r);if(d<0){L.style.display="none",u.style.cursor="default";return}const i=n[d];u.style.cursor=C&&i.key!==void 0?"pointer":"default",L.innerHTML=`<strong>${j(i.label)}</strong><br>`+j(`mean ${D(i.mean,s?.unit)} \xB7 \u03C3 ${D(i.stddev,s?.unit)}`)+`<br>n = ${i.count.toLocaleString()}`+(x!==null?"<br>"+j(`\u0394 vs ${U} ${i.mean-x>=0?"+":""}${D(i.mean-x,s?.unit)}`):"")+(C&&i.key!==void 0?"<br><em>click to open this wafer</em>":""),L.style.display="block",Te(L,l,r.clientX,r.clientY)}),u.addEventListener("mouseleave",()=>{L.style.display="none"}),u.addEventListener("click",r=>{const d=K(r);if(d<0||!C)return;const i=n[d];i.key!==void 0&&h!==null&&C(i.key,h)}),J(),M=ye(f,J)}return S(),{card:l,destroy:()=>{M?.disconnect(),L.remove(),l.remove()},setTest:o=>{Q.set(o)&&(h=o,S())},setAxisPrefs:o=>{$.set(o)&&S()}}}
@@ -0,0 +1,7 @@
1
+ import { toCanvas as toCanvasImpl } from './toCanvas.js';
2
+ import { buildDieListSection as buildDieListSectionImpl } from './dieList.js';
3
+ /** @deprecated Removed in 0.31.0. It belongs to the low-level drawing pipeline, which is being withdrawn: build with `buildWaferMap` and draw with `renderWaferMap` or `renderWaferGallery`. */
4
+ export declare const toCanvas: typeof toCanvasImpl;
5
+ /** @deprecated Removed in 0.31.0. `renderWaferMap` and `renderWaferGallery` show the die list themselves (the `dieList` option). */
6
+ export declare const buildDieListSection: typeof buildDieListSectionImpl;
7
+ //# sourceMappingURL=deprecated.d.ts.map
@@ -0,0 +1 @@
1
+ import{deprecated as e}from"../renderer/deprecate.js";import{toCanvas as i}from"./toCanvas.js";import{buildDieListSection as o}from"./dieList.js";const t="If you depend on it, say so at https://github.com/wafertools/wafermap/issues.",a="It belongs to the low-level drawing pipeline, which is being withdrawn: build with buildWaferMap and draw with renderWaferMap or renderWaferGallery.",r="renderWaferMap and renderWaferGallery show the die list themselves (the dieList option).";export const toCanvas=e(i,"toCanvas",`${a} ${t}`),buildDieListSection=e(o,"buildDieListSection",`${r} ${t}`);
@@ -1,5 +1,5 @@
1
1
  import type { Die } from '../core/dies.js';
2
- import { type Wafer } from '../core/index.js';
2
+ import type { Wafer } from '../core/index.js';
3
3
  import { type MetadataFieldDef, type TestDef } from '../renderer/buildWaferMap.js';
4
4
  import type { WaferMetadata } from '../core/metadata.js';
5
5
  import { type MetadataKeySelection } from '../stats/metadataColumns.js';
@@ -76,8 +76,6 @@ export interface DieListDisplayOptions {
76
76
  * directly into a long document).
77
77
  */
78
78
  maxHeight?: string;
79
- /** CSV filename, including extension. Default: `'dies.csv'`. */
80
- csvFilename?: string;
81
79
  }
82
80
  export interface DieListOptions extends DieListDisplayOptions {
83
81
  /** Heading text. Default: `Die list (N)`. */
@@ -1,13 +1,13 @@
1
- import{hasPosition as R,isPositionedDie as O}from"../core/dies.js";import{classifyDie as z}from"../core/index.js";import{isParametricTest as B,getTestPassStatus as x}from"../renderer/buildWaferMap.js";import{resolveMetadataColumns as V}from"../stats/metadataColumns.js";import{LEADING as D,TRACKING as W,wireControlHover as G,controlStyle as K,SPACE as X,RADIUS as F,FONT as C,CLR as d,saveTextFile as Y}from"./toolbar.js";import{csvField as I}from"./summaryPanel.js";import{fmt as T}from"../renderer/fmt.js";const _=5e4,k=new WeakSet;function Q(n){if(k.has(n))return;k.add(n);const o=n.createElement("style");o.textContent=`
1
+ import{hasPosition as R,isPositionedDie as O}from"../core/dies.js";import{classifyDie as z}from"../core/classify.js";import{isParametricTest as B,getTestPassStatus as x}from"../renderer/buildWaferMap.js";import{resolveMetadataColumns as V}from"../stats/metadataColumns.js";import{LEADING as D,TRACKING as W,wireControlHover as G,controlStyle as K,SPACE as X,RADIUS as I,FONT as C,CLR as c,saveTextFile as Y}from"./toolbar.js";import{csvField as T}from"./summaryPanel.js";import{fmt as k}from"../renderer/fmt.js";const _=5e4,F=new WeakSet;function Q(t){if(F.has(t))return;F.add(t);const o=t.createElement("style");o.textContent=`
2
2
  .wmap-dielist-table { width: 100%; border-collapse: collapse; font-size: 12px; font-variant-numeric: tabular-nums; }
3
3
  .wmap-dielist-th {
4
4
  text-align: left; padding: 4px 8px; position: sticky; top: 0;
5
- background: ${d.panelBg}; color: ${d.label}; font-weight: 600;
6
- border-bottom: 1px solid ${d.menuBorder}; white-space: nowrap;
5
+ background: ${c.panelBg}; color: ${c.label}; font-weight: 600;
6
+ border-bottom: 1px solid ${c.menuBorder}; white-space: nowrap;
7
7
  }
8
8
  .wmap-dielist-td {
9
- padding: 4px 8px; color: ${d.text};
10
- border-bottom: 1px solid ${d.menuBorder}; white-space: nowrap;
9
+ padding: 4px 8px; color: ${c.text};
10
+ border-bottom: 1px solid ${c.menuBorder}; white-space: nowrap;
11
11
  }
12
- `,n.head.appendChild(o)}function c(n,o,t,a){const i=n.createElement(o);return t&&Object.assign(i.style,t),a!==void 0&&(i.textContent=a),i}function U(n){return R(n)?String(n.x):""}function q(n){return R(n)?String(n.y):""}function J(n,o){if(o!==void 0)return o.filter(a=>a.testNumber!==void 0);const t=new Map;for(const a of n)for(const i of Object.keys(a.testValues??{})){const s=Number(i);t.has(s)||t.set(s,{testNumber:s,name:`Test ${s}`})}return[...t.values()].sort((a,i)=>a.testNumber-i.testNumber)}function Z(n,o,t){if(!o)return;const a=new Map;for(const i of n){if(!O(i))continue;const s=o(i);s&&a.set(i,z(i,s,{ringCount:t}))}return a.size>0?a:void 0}export function buildDieListSection(n,o,t={}){if(!n.length)return null;const a=t.ownerDocument??document;Q(a);const i=J(n,o),s=t.maxRows??_,S=Math.max(0,n.length-s),H=S>0?n.slice(0,s):n,m=c(a,"div",{display:"flex",flexDirection:"column",gap:X.md,flex:"1",minHeight:"0",minWidth:"0"}),w=c(a,"div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});w.appendChild(c(a,"div",{fontSize:C.meta,fontWeight:"700",letterSpacing:W,textTransform:"uppercase",color:d.label},t.title??`Die list (${n.length})`));const g=c(a,"button",{fontSize:C.body,padding:"3px 8px",borderRadius:F.control,...K("outlined"),background:d.menuBg},"Export CSV");g.type="button",G(g),w.appendChild(g),m.appendChild(w),t.note&&m.appendChild(c(a,"div",{fontSize:C.body,color:d.label,lineHeight:D.base,flexShrink:"0"},t.note));const v=Z(n,t.getWafer,t.ringCount??4),P=n.some(e=>e.edgeExcluded===!0),j=[...t.extraColumn?[t.extraColumn.label]:[],"X","Y","Ring","Quadrant","Edge excluded","Site","Hard bin","Soft bin",...i.map(e=>e.unit?`${e.name} (${e.unit})`:e.name)],{columns:M,truncatedKeys:h}=V({dies:n,waferMetadata:t.waferMetadata,metadataFields:t.metadataFields,dieKeys:t.metadataColumns,waferKeys:t.waferMetadataColumns==="none"?"none":void 0,waferPlacement:t.waferMetadataColumns??"csv",reservedLabels:j}),y=[...t.extraColumn?[{label:t.extraColumn.label,get:e=>t.extraColumn.get(e)??""}]:[],{label:"X",get:U},{label:"Y",get:q},...v?[{label:"Ring",get:e=>{const l=v.get(e);return l?String(l.ring):""}},{label:"Quadrant",get:e=>v.get(e)?.quadrant??""}]:[],...P?[{label:"Edge excluded",get:e=>e.edgeExcluded?"Yes":"No"}]:[],{label:"Site",get:e=>e.siteNum!==void 0?String(e.siteNum):""},{label:"Hard bin",get:e=>e.hbin!==void 0?String(e.hbin):""},{label:"Soft bin",get:e=>e.sbin!==void 0?String(e.sbin):""},...i.map(e=>{const l=!B(e);return{label:e.unit?`${e.name} (${e.unit})`:e.name,get:r=>{if(l){const p=x(r,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=r.testValues?.[e.testNumber];if(u!==void 0)return T(u,e.unit);const b=x(r,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"},csvGet:r=>{if(l){const p=x(r,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=r.testValues?.[e.testNumber];if(u!==void 0)return T(u,void 0,"engineering");const b=x(r,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"}}}),...M.map(e=>({label:e.label,get:e.get,csvOnly:e.csvOnly}))],N=y.filter(e=>!e.csvOnly),$=c(a,"div",{overflow:"auto",flex:"1",minHeight:"0",minWidth:"0",border:`1px solid ${d.menuBorder}`,borderRadius:F.control,...t.maxHeight?{maxHeight:t.maxHeight}:{}}),f=a.createElement("table");f.className="wmap-dielist-table",f.setAttribute("aria-label",t.title??`Die list (${n.length} dies)`);const E=c(a,"thead"),A=c(a,"tr");for(const e of N){const l=a.createElement("th");l.className="wmap-dielist-th",l.scope="col",l.textContent=e.label,A.appendChild(l)}E.appendChild(A),f.appendChild(E);const L=c(a,"tbody");for(const e of H){const l=c(a,"tr");for(const r of N){const u=a.createElement("td");u.className="wmap-dielist-td",u.textContent=r.get(e),l.appendChild(u)}L.appendChild(l)}if(f.appendChild(L),$.appendChild(f),m.appendChild($),S>0||h.length>0){const e=[];S>0&&e.push(`Showing the first ${s.toLocaleString()} of ${n.length.toLocaleString()} dies. The CSV export contains all ${n.length.toLocaleString()}.`),h.length>0&&e.push(`${h.length} further metadata field${h.length===1?"":"s"} not shown.`),m.appendChild(c(a,"div",{fontSize:C.body,color:d.label,lineHeight:D.base,flexShrink:"0"},e.join(" ")))}return g.addEventListener("click",()=>{const e=[y.map(l=>I(l.label)).join(",")];for(const l of n)e.push(y.map(r=>I((r.csvGet??r.get)(l))).join(","));Y(e.join(`
13
- `),t.csvFilename??"dies.csv","text/csv",t.onSaveText)}),m}
12
+ `,t.head.appendChild(o)}function d(t,o,n,a){const i=t.createElement(o);return n&&Object.assign(i.style,n),a!==void 0&&(i.textContent=a),i}function U(t){return R(t)?String(t.x):""}function q(t){return R(t)?String(t.y):""}function J(t,o){if(o!==void 0)return o.filter(a=>a.testNumber!==void 0);const n=new Map;for(const a of t)for(const i of Object.keys(a.testValues??{})){const s=Number(i);n.has(s)||n.set(s,{testNumber:s,name:`Test ${s}`})}return[...n.values()].sort((a,i)=>a.testNumber-i.testNumber)}function Z(t,o,n){if(!o)return;const a=new Map;for(const i of t){if(!O(i))continue;const s=o(i);s&&a.set(i,z(i,s,{ringCount:n}))}return a.size>0?a:void 0}export function buildDieListSection(t,o,n={}){if(!t.length)return null;const a=n.ownerDocument??document;Q(a);const i=J(t,o),s=n.maxRows??_,S=Math.max(0,t.length-s),H=S>0?t.slice(0,s):t,f=d(a,"div",{display:"flex",flexDirection:"column",gap:X.md,flex:"1",minHeight:"0",minWidth:"0"}),w=d(a,"div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});w.appendChild(d(a,"div",{fontSize:C.meta,fontWeight:"700",letterSpacing:W,textTransform:"uppercase",color:c.label},n.title??`Die list (${t.length})`));const g=d(a,"button",{fontSize:C.body,padding:"3px 8px",borderRadius:I.control,...K("outlined"),background:c.menuBg},"Export CSV");g.type="button",G(g),w.appendChild(g),f.appendChild(w),n.note&&f.appendChild(d(a,"div",{fontSize:C.body,color:c.label,lineHeight:D.base,flexShrink:"0"},n.note));const v=Z(t,n.getWafer,n.ringCount??4),P=t.some(e=>e.edgeExcluded===!0),j=[...n.extraColumn?[n.extraColumn.label]:[],"X","Y","Ring","Quadrant","Edge excluded","Site","Hard bin","Soft bin",...i.map(e=>e.unit?`${e.name} (${e.unit})`:e.name)],{columns:M,truncatedKeys:h}=V({dies:t,waferMetadata:n.waferMetadata,metadataFields:n.metadataFields,dieKeys:n.metadataColumns,waferKeys:n.waferMetadataColumns==="none"?"none":void 0,waferPlacement:n.waferMetadataColumns??"csv",reservedLabels:j}),y=[...n.extraColumn?[{label:n.extraColumn.label,get:e=>n.extraColumn.get(e)??""}]:[],{label:"X",get:U},{label:"Y",get:q},...v?[{label:"Ring",get:e=>{const l=v.get(e);return l?String(l.ring):""}},{label:"Quadrant",get:e=>v.get(e)?.quadrant??""}]:[],...P?[{label:"Edge excluded",get:e=>e.edgeExcluded?"Yes":"No"}]:[],{label:"Site",get:e=>e.siteNum!==void 0?String(e.siteNum):""},{label:"Hard bin",get:e=>e.hbin!==void 0?String(e.hbin):""},{label:"Soft bin",get:e=>e.sbin!==void 0?String(e.sbin):""},...i.map(e=>{const l=!B(e);return{label:e.unit?`${e.name} (${e.unit})`:e.name,get:r=>{if(l){const p=x(r,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=r.testValues?.[e.testNumber];if(u!==void 0)return k(u,e.unit);const b=x(r,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"},csvGet:r=>{if(l){const p=x(r,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=r.testValues?.[e.testNumber];if(u!==void 0)return k(u,void 0,"engineering");const b=x(r,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"}}}),...M.map(e=>({label:e.label,get:e.get,csvOnly:e.csvOnly}))],N=y.filter(e=>!e.csvOnly),$=d(a,"div",{overflow:"auto",flex:"1",minHeight:"0",minWidth:"0",border:`1px solid ${c.menuBorder}`,borderRadius:I.control,...n.maxHeight?{maxHeight:n.maxHeight}:{}}),m=a.createElement("table");m.className="wmap-dielist-table",m.setAttribute("aria-label",n.title??`Die list (${t.length} dies)`);const E=d(a,"thead"),A=d(a,"tr");for(const e of N){const l=a.createElement("th");l.className="wmap-dielist-th",l.scope="col",l.textContent=e.label,A.appendChild(l)}E.appendChild(A),m.appendChild(E);const L=d(a,"tbody");for(const e of H){const l=d(a,"tr");for(const r of N){const u=a.createElement("td");u.className="wmap-dielist-td",u.textContent=r.get(e),l.appendChild(u)}L.appendChild(l)}if(m.appendChild(L),$.appendChild(m),f.appendChild($),S>0||h.length>0){const e=[];S>0&&e.push(`Showing the first ${s.toLocaleString()} of ${t.length.toLocaleString()} dies. The CSV export contains all ${t.length.toLocaleString()}.`),h.length>0&&e.push(`${h.length} further metadata field${h.length===1?"":"s"} not shown.`),f.appendChild(d(a,"div",{fontSize:C.body,color:c.label,lineHeight:D.base,flexShrink:"0"},e.join(" ")))}return g.addEventListener("click",()=>{const e=[y.map(l=>T(l.label)).join(",")];for(const l of t)e.push(y.map(r=>T((r.csvGet??r.get)(l))).join(","));Y(e.join(`
13
+ `),"die-list.csv","text/csv",n.onSaveText)}),f}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Names for saved files — the one rule for every PNG and CSV the library saves.
3
+ *
4
+ * Every export used to be named for its content alone (`dies.csv`,
5
+ * `test-values.csv`, `wafermap.png`, a chart's title), so saving the same export
6
+ * from two wafers gave `dies.csv` and `dies (1).csv`, and nothing in the file
7
+ * name said which lot or wafer either one described. The library has that
8
+ * identity, so naming the file is its job, not the host's.
9
+ *
10
+ * The split: a save site names only its CONTENT (`die-list.csv`, the map's
11
+ * title) and keeps passing its `onSaveImage`/`onSaveText` handler as before.
12
+ * Each renderer wraps the host's handlers once with `withExportContext`, which
13
+ * prepends the context it knows — the host's `downloadFilename` prefix, the lot,
14
+ * the wafer or wafer count — read at save time, so the name follows
15
+ * `setResult`, `setItems` and mode changes. No save site threads identity.
16
+ *
17
+ * @internal Not exported from the package: hosts receive the result as the
18
+ * `suggestedName` argument of their save hooks.
19
+ */
20
+ import { type SaveImageHandler, type SaveTextHandler } from './toolbar.js';
21
+ /**
22
+ * Tell a host that passes `downloadFilename` its meaning changes in 0.31.0.
23
+ * Until then it keeps its documented meaning — the whole name of the map or
24
+ * gallery PNG — and nothing else reads it; changing that in a patch release
25
+ * would break hosts that match the name their `onSaveImage` receives.
26
+ */
27
+ export declare function noticeDownloadFilenameChange(downloadFilename: string | undefined): void;
28
+ /** What a renderer knows about the data behind an export. */
29
+ export interface ExportContext {
30
+ /**
31
+ * Leads the name, and suppresses any context it already names. Reserved for
32
+ * `downloadFilename` from 0.31.0; no renderer sets it before then (see
33
+ * `noticeDownloadFilenameChange`).
34
+ */
35
+ prefix?: string;
36
+ /** Distinct lot IDs covered, in display order. */
37
+ lots: string[];
38
+ /** One wafer's identity (`waferIdentityLabel`), when the export covers exactly one. */
39
+ wafer?: string;
40
+ /** Wafers covered, when the export covers several (a gallery or lot). */
41
+ waferCount?: number;
42
+ /** Set on a lot-stacked map: the wafers aggregated into it, or `true` when unknown. */
43
+ stackedWafers?: number | true;
44
+ }
45
+ /**
46
+ * The file name for an export: `prefix_lot_wafer_content.ext`, each part only
47
+ * when known and not already named by the prefix.
48
+ *
49
+ * @param ctx what the renderer knows (see `ExportContext`)
50
+ * @param contentName the save site's own name, with extension — e.g.
51
+ * `die-list.csv`, or a map title plus `.png`
52
+ */
53
+ export declare function buildExportFilename(ctx: ExportContext, contentName: string): string;
54
+ /**
55
+ * Wrap a renderer's save hooks so every export it makes is named by
56
+ * `buildExportFilename`. The returned handlers are always defined: with no host
57
+ * hook they perform the browser download themselves, so a save site cannot
58
+ * bypass the naming by falling through to its own default path.
59
+ *
60
+ * Hand the host's RAW hooks, never these, to anything that wraps again (a
61
+ * gallery card or detached window is its own renderer with its own context) —
62
+ * wrapping twice would name the lot twice.
63
+ */
64
+ export declare function withExportContext(getContext: () => ExportContext, onSaveImage: SaveImageHandler | undefined, onSaveText: SaveTextHandler | undefined): {
65
+ onSaveImage: SaveImageHandler;
66
+ onSaveText: SaveTextHandler;
67
+ };
68
+ //# sourceMappingURL=exportName.d.ts.map
@@ -0,0 +1 @@
1
+ import{downloadBlob as u}from"./toolbar.js";import{noticeOnce as _}from"../renderer/deprecate.js";export function noticeDownloadFilenameChange(e){e!=null&&_("downloadFilename",'downloadFilename becomes a prefix in 0.31.0: the library will append the lot, wafer and content (e.g. "<downloadFilename>_W05_hard-bin.png") and apply it to every saved file, CSVs included. Until then it names only the map or gallery PNG, as before.')}const $=60,m=180,g=/^(con|prn|aux|nul|com\d|lpt\d)$/i;function f(e){return e.normalize("NFC").replace(/[^\p{L}\p{N}._+-]+/gu,"-").replace(/-{2,}/g,"-").replace(/^[-._]+|[-._]+$/g,"").slice(0,$).replace(/[-._]+$/g,"")}function c(e,n){return/^\d+$/.test(n)?`${e}-${n}`:n}function w(e,n){const o=`_${e.toLowerCase().replace(/[-_.\s]+/g,"_")}_`,r=`_${n.toLowerCase().replace(/[-_.\s]+/g,"_")}_`;return o.includes(r)}export function buildExportFilename(e,n){const o=n.lastIndexOf("."),r=o>=0?n.slice(o):"",l=f(o>=0?n.slice(0,o):n).toLowerCase(),t=e.prefix?.trim()?f(e.prefix):"",a=[];if(e.lots.length===1?a.push({raw:e.lots[0],text:c("lot",e.lots[0])}):e.lots.length>1&&a.push({raw:`${e.lots.length}-lots`,text:`${e.lots.length}-lots`}),e.stackedWafers!==void 0){const i=e.stackedWafers===!0?"stacked":`stacked-${e.stackedWafers}-wafer${e.stackedWafers===1?"":"s"}`;a.push({raw:i,text:i})}else e.wafer!==void 0?a.push({raw:e.wafer,text:c("wafer",e.wafer)}):e.waferCount!==void 0&&e.waferCount>1&&a.push({raw:`${e.waferCount}-wafers`,text:`${e.waferCount}-wafers`});const p=[t];for(const{raw:i,text:h}of a){const d=f(h);d&&(t&&(w(t,f(i))||w(t,d))||p.push(d))}p.push(l);let s=p.filter(Boolean).join("_").slice(0,m).replace(/[-_.]+$/g,"");return s||(s="wafermap"),g.test(s)&&(s=`wafermap-${s}`),`${s}${r}`}export function withExportContext(e,n,o){return{onSaveImage:(r,l)=>{const t=buildExportFilename(e(),l);if(n)return n(r,t);u(r,t)},onSaveText:(r,l,t)=>{const a=buildExportFilename(e(),l);if(o)return o(r,a,t);u(new Blob([r],{type:t}),a)}}}
@@ -1,4 +1,4 @@
1
- export const ICONS={aggr:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 4H6l6 8-6 8h12"/></svg>',analysis:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg>',boxSelect:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3a2 2 0 0 0-2 2"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M21 19a2 2 0 0 1-2 2"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M9 3h1"/><path d="M9 21h1"/><path d="M14 3h1"/><path d="M14 21h1"/><path d="M3 9v1"/><path d="M21 9v1"/><path d="M3 14v1"/><path d="M21 14v1"/></svg>',close:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',columns:`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1
+ export const ICONS={aggr:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 4H6l6 8-6 8h12"/></svg>',analysis:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg>',boxSelect:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3a2 2 0 0 0-2 2"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M21 19a2 2 0 0 1-2 2"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M9 3h1"/><path d="M9 21h1"/><path d="M14 3h1"/><path d="M14 21h1"/><path d="M3 9v1"/><path d="M21 9v1"/><path d="M3 14v1"/><path d="M21 14v1"/></svg>',close:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',collapse:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m7 20 5-5 5 5"/><path d="m7 4 5 5 5-5"/></svg>',columns:`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
2
2
  <rect x="3" y="3" width="4" height="18" rx="1"/>
3
3
  <rect x="10" y="3" width="4" height="18" rx="1"/>
4
4
  <rect x="17" y="3" width="4" height="18" rx="1"/>
@@ -13,7 +13,7 @@ export const ICONS={aggr:'<svg viewBox="0 0 24 24" width="16" height="16" fill="
13
13
  <path d="M3 12 c0 2.5 4 4 9 4 s9-1.5 9-4" />
14
14
 
15
15
  <path d="M3 17 c0 2.5 4 4 9 4 s9-1.5 9-4" />
16
- </svg>`,orient:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76" fill="currentColor" stroke="none"/></svg>',overlays:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>',palette:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m14.622 17.897-10.68-2.913"/><path d="M18.376 2.622a1 1 0 1 1 3.002 3.002L17.36 9.643a.5.5 0 0 0 0 .707l.944.944a2.41 2.41 0 0 1 0 3.408l-.944.944a.5.5 0 0 1-.707 0L8.354 7.348a.5.5 0 0 1 0-.707l.944-.944a2.41 2.41 0 0 1 3.408 0l.944.944a.5.5 0 0 0 .707 0z"/><path d="M9 8c-1.804 2.71-3.97 3.46-6.583 3.948a.507.507 0 0 0-.302.819l7.32 8.883a1 1 0 0 0 1.185.204C12.735 20.405 16 16.792 16 15"/></svg>',pan:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20"/><path d="m15 19-3 3-3-3"/><path d="m19 9 3 3-3 3"/><path d="M2 12h20"/><path d="m5 9-3 3 3 3"/><path d="m9 5 3-3 3 3"/></svg>',print:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><path d="M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"/><rect x="6" y="14" width="12" height="8" rx="1"/></svg>',quadrants:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><line x1="12" y1="3" x2="12" y2="21"/><line x1="3" y1="12" x2="21" y2="12"/></svg>',reset:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>',reticle:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="6" rx="0.5"/><rect x="13" y="3" width="8" height="6" rx="0.5"/><rect x="3" y="11" width="8" height="6" rx="0.5"/><rect x="13" y="11" width="8" height="6" rx="0.5"/><rect x="3" y="19" width="8" height="2" rx="0.5" stroke-dasharray="2 1"/><rect x="13" y="19" width="8" height="2" rx="0.5" stroke-dasharray="2 1"/></svg>',rings:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5.5"/><circle cx="12" cy="12" r="2" fill="currentColor" stroke="none"/></svg>',rotateCW:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>',specRange:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4 L5 20"/><path d="M5 4 L8 4"/><path d="M5 20 L8 20"/><path d="M19 4 L19 20"/><path d="M19 4 L16 4"/><path d="M19 20 L16 20"/><path d="M5 12 L19 12" stroke-dasharray="3 2"/></svg>',wafer:`<svg
16
+ </svg>`,orient:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76" fill="currentColor" stroke="none"/></svg>',overlays:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>',palette:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m14.622 17.897-10.68-2.913"/><path d="M18.376 2.622a1 1 0 1 1 3.002 3.002L17.36 9.643a.5.5 0 0 0 0 .707l.944.944a2.41 2.41 0 0 1 0 3.408l-.944.944a.5.5 0 0 1-.707 0L8.354 7.348a.5.5 0 0 1 0-.707l.944-.944a2.41 2.41 0 0 1 3.408 0l.944.944a.5.5 0 0 0 .707 0z"/><path d="M9 8c-1.804 2.71-3.97 3.46-6.583 3.948a.507.507 0 0 0-.302.819l7.32 8.883a1 1 0 0 0 1.185.204C12.735 20.405 16 16.792 16 15"/></svg>',pan:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20"/><path d="m15 19-3 3-3-3"/><path d="m19 9 3 3-3 3"/><path d="M2 12h20"/><path d="m5 9-3 3 3 3"/><path d="m9 5 3-3 3 3"/></svg>',print:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><path d="M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"/><rect x="6" y="14" width="12" height="8" rx="1"/></svg>',quadrants:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><line x1="12" y1="3" x2="12" y2="21"/><line x1="3" y1="12" x2="21" y2="12"/></svg>',reset:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>',reticle:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="6" rx="0.5"/><rect x="13" y="3" width="8" height="6" rx="0.5"/><rect x="3" y="11" width="8" height="6" rx="0.5"/><rect x="13" y="11" width="8" height="6" rx="0.5"/><rect x="3" y="19" width="8" height="2" rx="0.5" stroke-dasharray="2 1"/><rect x="13" y="19" width="8" height="2" rx="0.5" stroke-dasharray="2 1"/></svg>',rings:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5.5"/><circle cx="12" cy="12" r="2" fill="currentColor" stroke="none"/></svg>',rotateCW:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>',specRange:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4 L5 20"/><path d="M5 4 L8 4"/><path d="M5 20 L8 20"/><path d="M19 4 L19 20"/><path d="M19 4 L16 4"/><path d="M19 20 L16 20"/><path d="M5 12 L19 12" stroke-dasharray="3 2"/></svg>',uncollapse:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>',wafer:`<svg
17
17
  viewBox="0 0 24 24"
18
18
  width="16"
19
19
  height="16"
@@ -1,12 +1,10 @@
1
1
  export { WMAP_VERSION, WMAP_BUILD_TIME } from './version.js';
2
- export { toCanvas } from './toCanvas.js';
3
2
  export type { ToCanvasOptions, ToCanvasResult, HitTarget, ViewportTransform } from './toCanvas.js';
4
3
  export { renderWaferMap } from './renderWaferMap.js';
5
4
  export type { RenderOptions, WaferMapController, WaferViewOptions, WaferPreferences, WaferDisplayState } from './renderWaferMap.js';
6
5
  export { renderWaferGallery } from './renderWaferGallery.js';
7
6
  export type { WaferMapDisplayItem, WaferMapDisplayItemFactory, GalleryOptions, GalleryController } from './renderWaferGallery.js';
8
7
  export type { SummaryPanelOptions, FindingsNotice } from './summaryPanel.js';
9
- export { buildDieListSection } from './dieList.js';
10
8
  export type { DieListOptions, DieListDisplayOptions } from './dieList.js';
11
9
  export { collectWarnings, severityOf } from './warnings.js';
12
10
  export type { WarningsOptions } from './warnings.js';
@@ -16,6 +14,7 @@ export type { DetachWindowOpener } from './toolbar.js';
16
14
  export type { UserGuideExtension } from './toolbar.js';
17
15
  export { openWaferMapGuide } from './toolbar.js';
18
16
  export { ICONS } from './icons.js';
19
- export { openReportModal } from './toolbar.js';
20
17
  export type { OverlayHandle } from './toolbar.js';
18
+ export { openReportModal } from './toolbar.js';
19
+ export { toCanvas, buildDieListSection, } from './deprecated.js';
21
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- export{WMAP_VERSION,WMAP_BUILD_TIME}from"./version.js";export{toCanvas}from"./toCanvas.js";export{renderWaferMap}from"./renderWaferMap.js";export{renderWaferGallery}from"./renderWaferGallery.js";export{buildDieListSection}from"./dieList.js";export{collectWarnings,severityOf}from"./warnings.js";export{setDetachWindowOpener}from"./toolbar.js";export{openWaferMapGuide}from"./toolbar.js";export{ICONS}from"./icons.js";export{openReportModal}from"./toolbar.js";
1
+ export{WMAP_VERSION,WMAP_BUILD_TIME}from"./version.js";export{renderWaferMap}from"./renderWaferMap.js";export{renderWaferGallery}from"./renderWaferGallery.js";export{collectWarnings,severityOf}from"./warnings.js";export{setDetachWindowOpener}from"./toolbar.js";export{openWaferMapGuide}from"./toolbar.js";export{ICONS}from"./icons.js";export{openReportModal}from"./toolbar.js";export{toCanvas,buildDieListSection}from"./deprecated.js";
@@ -38,7 +38,6 @@ export interface InsightsTabDeps {
38
38
  * picked up, and so a bin is the same colour in a chart as on the map.
39
39
  */
40
40
  getBinColors: () => BinColors;
41
- passBins: number[];
42
41
  /** Read fresh each render — used by the Overview tab's ring/quadrant regional yield cards. Default 4. */
43
42
  getRingCount?: () => number;
44
43
  onSaveImage?: SaveImageHandler;