@wafertools/wafermap 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/AGENTS.md +7 -7
  2. package/CHANGELOG.md +374 -1
  3. package/README.md +5 -5
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +15 -10
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +7 -0
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/binCluster.d.ts +7 -1
  9. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +57 -15
  13. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/correlation.js +1 -2
  15. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +2 -0
  16. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/histogram.d.ts +16 -0
  18. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  19. package/dist/packages/canvas-adapter/charts/palette.d.ts +2 -0
  20. package/dist/packages/canvas-adapter/charts/palette.js +1 -1
  21. package/dist/packages/canvas-adapter/charts/plotChart.d.ts +27 -0
  22. package/dist/packages/canvas-adapter/charts/plotChart.js +1 -0
  23. package/dist/packages/canvas-adapter/charts/pointInteractions.d.ts +45 -0
  24. package/dist/packages/canvas-adapter/charts/pointInteractions.js +1 -0
  25. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +2 -0
  26. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  27. package/dist/packages/canvas-adapter/charts/scatter.d.ts +19 -0
  28. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  29. package/dist/packages/canvas-adapter/charts/sweep.d.ts +12 -0
  30. package/dist/packages/canvas-adapter/charts/sweep.js +1 -1
  31. package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +7 -0
  32. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  33. package/dist/packages/canvas-adapter/charts/trend.d.ts +11 -0
  34. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  35. package/dist/packages/canvas-adapter/confirmDialog.d.ts +13 -0
  36. package/dist/packages/canvas-adapter/confirmDialog.js +1 -0
  37. package/dist/packages/canvas-adapter/dataTab.d.ts +131 -0
  38. package/dist/packages/canvas-adapter/dataTab.js +1 -0
  39. package/dist/packages/canvas-adapter/dieList.d.ts +39 -4
  40. package/dist/packages/canvas-adapter/dieList.js +6 -7
  41. package/dist/packages/canvas-adapter/drilldown.d.ts +20 -3
  42. package/dist/packages/canvas-adapter/drilldown.js +1 -1
  43. package/dist/packages/canvas-adapter/guideWindow.d.ts +96 -0
  44. package/dist/packages/canvas-adapter/guideWindow.js +1 -0
  45. package/dist/packages/canvas-adapter/identityHeader.d.ts +16 -0
  46. package/dist/packages/canvas-adapter/identityHeader.js +1 -1
  47. package/dist/packages/canvas-adapter/index.d.ts +3 -3
  48. package/dist/packages/canvas-adapter/index.js +1 -1
  49. package/dist/packages/canvas-adapter/insightsTab.d.ts +73 -54
  50. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  51. package/dist/packages/canvas-adapter/maplessSummary.d.ts +2 -2
  52. package/dist/packages/canvas-adapter/plotEditor.d.ts +29 -0
  53. package/dist/packages/canvas-adapter/plotEditor.js +1 -0
  54. package/dist/packages/canvas-adapter/plotItems.d.ts +32 -0
  55. package/dist/packages/canvas-adapter/plotItems.js +1 -0
  56. package/dist/packages/canvas-adapter/plotModal.d.ts +30 -0
  57. package/dist/packages/canvas-adapter/plotModal.js +1 -0
  58. package/dist/packages/canvas-adapter/plotStore.d.ts +52 -0
  59. package/dist/packages/canvas-adapter/plotStore.js +1 -0
  60. package/dist/packages/canvas-adapter/plotTab.d.ts +30 -0
  61. package/dist/packages/canvas-adapter/plotTab.js +1 -0
  62. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +3 -2
  63. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  64. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +51 -2
  65. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  66. package/dist/packages/canvas-adapter/summaryPanel.d.ts +49 -11
  67. package/dist/packages/canvas-adapter/summaryPanel.js +2 -3
  68. package/dist/packages/canvas-adapter/summaryRail.d.ts +7 -0
  69. package/dist/packages/canvas-adapter/summaryRail.js +1 -0
  70. package/dist/packages/canvas-adapter/sweepEditor.d.ts +17 -0
  71. package/dist/packages/canvas-adapter/sweepEditor.js +1 -0
  72. package/dist/packages/canvas-adapter/tableExport.d.ts +22 -0
  73. package/dist/packages/canvas-adapter/tableExport.js +3 -0
  74. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  75. package/dist/packages/canvas-adapter/toolbar.d.ts +74 -93
  76. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  77. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  78. package/dist/packages/canvas-adapter/userGuideHtml.js +409 -51
  79. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  80. package/dist/packages/canvas-adapter/version.js +1 -1
  81. package/dist/packages/canvas-adapter/viewSwitch.d.ts +13 -0
  82. package/dist/packages/canvas-adapter/viewSwitch.js +1 -0
  83. package/dist/packages/canvas-adapter/virtualTable.d.ts +44 -0
  84. package/dist/packages/canvas-adapter/virtualTable.js +1 -0
  85. package/dist/packages/core/aggregates.d.ts +6 -0
  86. package/dist/packages/core/aggregates.js +1 -1
  87. package/dist/packages/core/classify.d.ts +3 -2
  88. package/dist/packages/core/classify.js +1 -1
  89. package/dist/packages/core/compact.d.ts +205 -0
  90. package/dist/packages/core/compact.js +3 -0
  91. package/dist/packages/core/passBins.d.ts +23 -4
  92. package/dist/packages/core/passBins.js +1 -1
  93. package/dist/packages/core/ringCount.d.ts +22 -0
  94. package/dist/packages/core/ringCount.js +1 -0
  95. package/dist/packages/core/tableCsv.d.ts +43 -0
  96. package/dist/packages/core/tableCsv.js +3 -0
  97. package/dist/packages/renderer/axisTicks.d.ts +14 -0
  98. package/dist/packages/renderer/axisTicks.js +1 -1
  99. package/dist/packages/renderer/binColors.d.ts +6 -6
  100. package/dist/packages/renderer/binColors.js +1 -1
  101. package/dist/packages/renderer/buildView.d.ts +27 -2
  102. package/dist/packages/renderer/buildView.js +1 -1
  103. package/dist/packages/renderer/buildWaferMap.js +1 -1
  104. package/dist/packages/renderer/compactView.d.ts +22 -0
  105. package/dist/packages/renderer/compactView.js +1 -0
  106. package/dist/packages/renderer/xyIndicator.d.ts +27 -0
  107. package/dist/packages/renderer/xyIndicator.js +1 -0
  108. package/dist/packages/stats/analyzeWaferLot.d.ts +28 -0
  109. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  110. package/dist/packages/stats/analyzeWaferMap.d.ts +36 -1
  111. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  112. package/dist/packages/stats/binPareto.d.ts +1 -1
  113. package/dist/packages/stats/binPareto.js +1 -1
  114. package/dist/packages/stats/binRows.d.ts +38 -0
  115. package/dist/packages/stats/binRows.js +1 -0
  116. package/dist/packages/stats/filterFindings.d.ts +25 -0
  117. package/dist/packages/stats/filterFindings.js +1 -1
  118. package/dist/packages/stats/findingText.d.ts +9 -0
  119. package/dist/packages/stats/findingText.js +2 -0
  120. package/dist/packages/stats/histogram.d.ts +27 -0
  121. package/dist/packages/stats/histogram.js +1 -1
  122. package/dist/packages/stats/index.d.ts +2 -0
  123. package/dist/packages/stats/index.js +1 -1
  124. package/dist/packages/stats/lotDrift.d.ts +16 -0
  125. package/dist/packages/stats/lotDrift.js +1 -0
  126. package/dist/packages/stats/lotPattern.d.ts +33 -0
  127. package/dist/packages/stats/lotPattern.js +1 -0
  128. package/dist/packages/stats/math.d.ts +60 -6
  129. package/dist/packages/stats/math.js +1 -1
  130. package/dist/packages/stats/patternClassification.d.ts +32 -1
  131. package/dist/packages/stats/patternClassification.js +1 -1
  132. package/dist/packages/stats/plotData.d.ts +258 -0
  133. package/dist/packages/stats/plotData.js +1 -0
  134. package/dist/packages/stats/plotId.d.ts +3 -0
  135. package/dist/packages/stats/plotId.js +1 -0
  136. package/dist/packages/stats/plotSpec.d.ts +119 -0
  137. package/dist/packages/stats/plotSpec.js +4 -0
  138. package/dist/packages/stats/presentation.d.ts +44 -0
  139. package/dist/packages/stats/presentation.js +1 -0
  140. package/dist/packages/stats/regions.d.ts +15 -0
  141. package/dist/packages/stats/regions.js +1 -1
  142. package/dist/packages/stats/renderSummaryReport.d.ts +23 -5
  143. package/dist/packages/stats/renderSummaryReport.js +32 -27
  144. package/dist/packages/stats/reportHtml.d.ts +54 -9
  145. package/dist/packages/stats/reportHtml.js +145 -55
  146. package/dist/packages/stats/scatter.d.ts +6 -0
  147. package/dist/packages/stats/scatter.js +1 -1
  148. package/dist/packages/stats/sweep.d.ts +37 -1
  149. package/dist/packages/stats/sweep.js +1 -1
  150. package/dist/packages/stats/sweepText.d.ts +14 -0
  151. package/dist/packages/stats/sweepText.js +1 -0
  152. package/dist/packages/stats/sweepXFromName.d.ts +14 -0
  153. package/dist/packages/stats/sweepXFromName.js +1 -1
  154. package/dist/packages/stats/synthesis.d.ts +76 -0
  155. package/dist/packages/stats/synthesis.js +2 -0
  156. package/dist/packages/stats/testPassRate.d.ts +6 -0
  157. package/dist/packages/stats/testPassRate.js +1 -1
  158. package/dist/packages/stats/trend.js +1 -1
  159. package/dist/packages/stats/types.d.ts +2 -2
  160. package/dist/packages/stats/yield.d.ts +15 -6
  161. package/dist/packages/stats/yield.js +1 -1
  162. package/dist/packages/stats/yieldRows.d.ts +31 -0
  163. package/dist/packages/stats/yieldRows.js +1 -0
  164. package/llms.txt +16 -4
  165. package/package.json +3 -3
  166. package/dist/packages/stats/findingsNarrative.d.ts +0 -3
  167. package/dist/packages/stats/findingsNarrative.js +0 -1
@@ -1 +1 @@
1
- import{buildBinClusterData as H}from"../../stats/binPareto.js";import{SPACE as T,FONT as w,CLR as E}from"../toolbar.js";import{cardShell as L,makeTooltip as $,makeSegmented as D,renderEmptyState as I}from"./chartShell.js";import{renderGroupedBarPlot as M}from"./groupedBarPlot.js";import{escHtml as g}from"../../core/utils.js";const R=90,_=8;export function renderBinClusterPanel(s){const{groups:f,onSaveImage:h}=s;let n="hbin",c=s.title??"Hard bin pareto";const{card:o,heading:S,body:r,controlsRow:y}=L(c,h,s.ownerDocument);y.appendChild(D([["hbin","Hard bins"],["sbin","Soft bins"]],n,e=>{n=e,c=`${n==="hbin"?"Hard":"Soft"} bin pareto`,S.textContent=c,m()},o.ownerDocument));const u=o.ownerDocument.createElement("div");u.textContent="One cluster per bin \xB7 a sub-bar per group",Object.assign(u.style,{color:E.label,fontSize:w.body,marginBottom:T.sm}),o.insertBefore(u,r);const B=$(o);let l=null;function m(){l?.destroy(),l=null,r.innerHTML="";const e=H(f,n);if(e.bins.length===0||e.groups.length===0){I(r,"No bin data available for the current grouping.");return}const p=e.groups,b=e.bins,C=Math.max(1,...b.flatMap(t=>t.counts));l=M(o,r,B,{rows:b.map(t=>({label:t.label,bars:t.counts.map(a=>({fraction:a/C})),trailing:`${t.total}`})),groups:p,labelWidth:R,maxVisibleRows:_,tooltipHtml:(t,a)=>{const i=b[t],d=i.counts[a],x=i.total>0?d/i.total*100:0;return`<strong>${g(i.label)}</strong> \xB7 ${g(String(p[a]))}<br>${d} dies (${x.toFixed(1)}% of bin)`}})}return m(),{card:o,destroy:()=>l?.destroy()}}
1
+ import{buildBinClusterData as x}from"../../stats/binPareto.js";import{SPACE as H,FONT as T,CLR as E}from"../toolbar.js";import{cardShell as L,makeTooltip as $,makeSegmented as D,renderEmptyState as I}from"./chartShell.js";import{renderGroupedBarPlot as R}from"./groupedBarPlot.js";import{escHtml as g}from"../../core/utils.js";const k=90,M=8;export function renderBinClusterPanel(e){const{groups:f,onSaveImage:S}=e;let i="hbin",u=e.title??"Hard bin pareto";const{card:n,heading:h,body:l,controlsRow:B}=L(u,S,e.ownerDocument);B.appendChild(D([["hbin","Hard bins"],["sbin","Soft bins"]],i,o=>{i=o,u=`${i==="hbin"?"Hard":"Soft"} bin pareto`,h.textContent=u,m()},n.ownerDocument));const s=n.ownerDocument.createElement("div");s.dataset.wmapCaption="1",s.textContent="One cluster per bin \xB7 a sub-bar per group"+(e.onSelectBin?" \xB7 click a sub-bar to chart or tabulate its dies":""),Object.assign(s.style,{color:E.label,fontSize:T.body,marginBottom:H.sm}),n.insertBefore(s,l);const C=$(n);let c=null;function m(){c?.destroy(),c=null,l.innerHTML="";const o=x(f,i);if(o.bins.length===0||o.groups.length===0){I(l,"No bin data available for the current grouping.");return}const d=o.groups,b=o.bins,y=Math.max(1,...b.flatMap(t=>t.counts));c=R(n,l,C,{rows:b.map(t=>({label:t.label,bars:t.counts.map(r=>({fraction:r/y})),trailing:`${t.total}`})),groups:d,labelWidth:k,maxVisibleRows:M,onRowClick:e.onSelectBin?(t,r,a)=>e.onSelectBin({bin:b[t].binCode,binType:i,groupKey:d[r]},a):void 0,tooltipHtml:(t,r)=>{const a=b[t],p=a.counts[r],w=a.total>0?p/a.total*100:0;return`<strong>${g(a.label)}</strong> \xB7 ${g(String(d[r]))}<br>${p} dies (${w.toFixed(1)}% of bin)`}})}return m(),{card:n,destroy:()=>c?.destroy()}}
@@ -1 +1 @@
1
- import{buildTestBoxplotData as rt}from"../../stats/boxplot.js";import{SPACE as He,fontPx as _,FONT as ct,CLR as ft}from"../toolbar.js";import{fmt as ge}from"../../renderer/fmt.js";import{fitTicks as ut}from"../../renderer/axisTicks.js";import{QUANTITY as De}from"./palette.js";import{cardShell as mt,observeResize as dt,makeTooltip as xt,positionChartTooltip as pt,makeBackButton as ht,makeLinkedTestSelect as gt,makeToggle as bt,makeLinkedAxisPrefs as yt,renderEmptyState as Ee,fitRowsHeight as Tt,setChartGrow as wt,resolveChartCanvasColors as vt,makeAxisFormat as Oe,horizontalTickSpacing as kt,resolveAxisRange as Mt,shouldIncludeLimitsByDefault as Lt,drawOffAxisLimits as Ct,limitLabelSide as At,limitLines as We,limitExtent as $t,hasBothLimitKinds as St,stackLabelRows as It,strokeLimitLine as Bt,PADDING as M,VALUE_WIDTH as Rt,WAFER_MENU_HINT as Ht,prepareCanvas as Dt}from"./chartShell.js";import{escHtml as L,maxOf as Et,minOf as Ot}from"../../core/utils.js";const C=24,A=5,Pe=110,Wt=12,_e=20;export function renderBoxplotPanel(u){const{title:te="Test value distribution",items:Ge,testDefs:be,onSaveImage:Ne,groups:g,groupLabelText:ne="group",onOpen:Q}=u,Xe=u.openActionLabel??"open that wafer",qe=u.openActionLabel??"open this wafer",{card:a,heading:Ye,body:x,controlsRow:U}=mt(te,Ne,u.ownerDocument);wt(a,"rows"),a.style.flex="0 0 auto",x.style.flex="0 0 auto",a.style.alignSelf="start";const oe=be.filter(i=>i.testNumber!==void 0);let T=u.selectedTestNumber??oe[0]?.testNumber??null,ie=!1,F=0,b=null,D=null,le=null,ye=null;function ze(){const i=!g||g.length===0?"":b??"\0overview";if(le&&ye===i)return le;const p=!g||g.length===0?Ge:b!==null?g.find(v=>v.key===b)?.items??[]:g.map(v=>({label:v.key,dies:v.items.flatMap(S=>S.dies??[])}));return le=p,ye=i,p}function Te(){b!==null&&!D?(D=ht(()=>{we(null),u.onGroupChange?.(null)},a.ownerDocument),U.appendChild(D)):b===null&&D&&(D.remove(),D=null),Ye.textContent=b!==null?`${te} \u2014 ${ne}: ${b}`:te}function we(i){return b===i||i!==null&&!(g??[]).some(p=>p.key===i)?!1:(b=i,Te(),V(),$(),!0)}const ve=gt(oe,T,i=>{T=i,$(),u.onTestChange?.(i)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Qe=ve.el;U.appendChild(Qe),U.appendChild(bt("Log scale",ie,i=>{ie=i,$()},a.ownerDocument));const se=a.ownerDocument.createElement("span");Object.assign(se.style,{display:"inline-flex",gap:He.lg,alignItems:"center"}),U.appendChild(se);const ae=yt(se,u.axisPrefs,i=>{u.onAxisPrefsChange?.(i),$()},a.ownerDocument),re=a.ownerDocument.createElement("div");Object.assign(re.style,{color:ft.label,fontSize:ct.body,marginBottom:He.sm}),a.insertBefore(re,x);function V(){const i=!!g&&g.length>0&&b===null,p=[];i?p.push(`click a ${ne}'s box to see it by wafer`):Q&&p.push(`click a box to ${Xe}`);const v=p.length?`${p[0][0].toUpperCase()}${p[0].slice(1)} \xB7 `:"";re.textContent=`${v}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(F?` \xB7 axis clipped, ${F} value${F===1?"":"s"} outside`:"")}V(),Te();const E=xt(a);let ce=null;function $(){const{includeLimits:i,clipOutliers:p,limits:v}=ae.get();if(V(),x.innerHTML="",oe.length===0||T===null){Ee(x,"No parametric test data available for box plots.");return}const S=ze(),h=rt(S,T),j=be.find(t=>t.testNumber===T),O=j?.unit,fe=We(j,v),{lo:Ve,hi:je}=$t(fe);if(h.every(t=>t.count===0)){Ee(x,"No parametric test data available for box plots.");return}const m=a.ownerDocument.createElement("canvas");m.style.display="block",m.style.cursor="default",x.appendChild(m);const ke=M*2+Math.min(h.length,Wt)*(C+A)+_e;x.style.maxHeight=`${ke}px`,x.style.overflowY="auto",x.style.scrollbarGutter="stable";let G=-1;const ue=h.filter(t=>t.count>0),Me=Ot(ue.map(t=>t.min)),Le=Et(ue.map(t=>t.max)),Ce=i??Lt(Me,Le,Ve,je);ae.sync(Ce,We(j).length>0,St(j));const N=Mt({dataMin:Me,dataMax:Le,limits:fe,includeLimits:Ce,clipOutliers:p,values:ue.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});F=N.clippedCount,V();const W=N.lo,K=N.hi,Ke=K-W||1,J=ie&&W>0,me=J?Math.log10(W):0,Ae=(J?Math.log10(K):0)-me||1,de=Math.max(Math.abs(W),Math.abs(K));function Je(){const t=M+Pe,s=m.clientWidth-t-Rt-M;return{plotX:t,plotMaxWidth:Math.max(10,s)}}function I(t,s,n){return J?s+(Math.log10(t)-me)/Ae*n:s+(t-W)/Ke*n}function Ze(t,s){if(J){const d=[];for(let e=0;e<=4;e++)d.push(Math.pow(10,me+Ae*e/4));return{ticks:d,axis:Oe(de,O)}}const n=ut(W,K,s,kt(t,de,O));return{ticks:n.ticks,axis:Oe(de,O,n.step||void 0)}}function Z(){const t=vt(a),s=M*2+h.length*(C+A)+_e;x.style.maxHeight=`${Tt(a,x,ke,s)}px`;const n=x.clientWidth,d=Dt(m,a,n,s);if(!d)return;const{ctx:e}=d;e.font=`${_(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:c}=Je();h.forEach((o,f)=>{const y=M+f*(C+A),r=y+C/2;f===G&&(e.fillStyle=t.bgHover,e.fillRect(0,y-A/2,n,C+A)),e.fillStyle=t.text,e.textAlign="right";const ee=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(ee,M+Pe-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const it=l+c,q=at=>Math.min(it,Math.max(l,at)),Ie=I(o.min,l,c),Be=I(o.max,l,c),B=q(Ie),Y=q(I(o.q1,l,c)),Re=q(I(o.median,l,c)),he=q(I(o.q3,l,c)),R=q(Be),lt=Ie<B,st=Be>R,H=y+3,z=y+C-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(B,r),e.lineTo(Y,r),e.moveTo(he,r),e.lineTo(R,r);const k=3;lt?(e.moveTo(B+k,r-k),e.lineTo(B,r),e.lineTo(B+k,r+k)):(e.moveTo(B,H),e.lineTo(B,z)),st?(e.moveTo(R-k,r-k),e.lineTo(R,r),e.lineTo(R-k,r+k)):(e.moveTo(R,H),e.lineTo(R,z)),e.stroke(),e.fillStyle=De,e.globalAlpha=.35,e.fillRect(Y,H,Math.max(1,he-Y),z-H),e.globalAlpha=1,e.strokeStyle=De,e.strokeRect(Y,H,Math.max(1,he-Y),z-H),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Re,H),e.lineTo(Re,z),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(ge(o.median,O,"engineering"),l+c+10,r)});const w=M+h.length*(C+A);e.font=`${_(-1)}px system-ui, sans-serif`,Ct(e,N.offAxis,{left:l,right:l+c,top:0,bottom:w},"horizontal",t.limitLine,o=>ge(o,O,"engineering"));const et=new Set(N.offAxis.map(o=>o.value)),Se=fe.filter(o=>!et.has(o.value)).map(o=>{const f=I(o.value,l,c),y=e.measureText(o.label).width,r=At(f,y,l,l+c,o.end==="lo"),ee=r<0?f-3-y:f+3;return{l:o,x:f,dir:r,start:ee,end:ee+y}}),tt=It(Se),nt=_(-1)+2;Se.forEach(({l:o,x:f,dir:y},r)=>{Bt(e,o,t.limitLine,f,0,f,w),e.fillStyle=t.limitLine,e.textAlign=y<0?"right":"left",e.textBaseline="bottom",e.fillText(o.label,f+y*3,w-1-tt[r]*nt)}),e.font=`${_(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,w),e.lineTo(l+c,w),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:ot,axis:pe}=Ze(e,c);for(const o of ot){const f=I(o,l,c);e.beginPath(),e.moveTo(f,w),e.lineTo(f,w+4),e.stroke(),e.fillText(pe.tick(o),f,w+6)}pe.unitLabel&&(e.textAlign="left",e.font=`${_(-1)}px system-ui, sans-serif`,e.fillText(`(${pe.unitLabel})`,l+c+22,w+6),e.font=`${_(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function xe(t){const s=Math.floor((t-M+A/2)/(C+A));return s>=0&&s<h.length?s:-1}function X(t){return ge(t,O,"engineering")}const P=!!g&&g.length>0&&b===null,$e=t=>!P&&!!Q&&S[t]?.waferIndex!==void 0;m.addEventListener("mousemove",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top),d=n>=0&&h[n].count>0&&(P||$e(n));if(n!==G&&(G=n,m.style.cursor=d?"pointer":"default",Z()),n>=0&&h[n].count>0){const e=h[n],l=P?`<br><em>click to see this ${L(ne)} by wafer</em>`:$e(n)?`<br><em>click to ${L(qe)}</em>`:"",c=!P&&u.onWaferContextMenu&&S[n]?.waferIndex!==void 0?Ht:"";E.innerHTML=`<strong>${L(e.label)}</strong> (${e.count} dies)<br>max ${L(X(e.max))}<br>q3 ${L(X(e.q3))}<br>median ${L(X(e.median))}<br>q1 ${L(X(e.q1))}<br>min ${L(X(e.min))}${l}${c}`,E.style.display="block",pt(E,a,t.clientX,t.clientY)}else E.style.display="none"}),m.addEventListener("mouseleave",()=>{G!==-1&&(G=-1,Z()),E.style.display="none"}),m.addEventListener("click",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top);if(n===-1||h[n].count===0)return;if(P){const e=h[n].label;we(e),u.onGroupChange?.(e);return}const d=S[n]?.waferIndex;Q&&d!==void 0&&T!==null&&Q(d,T)}),m.addEventListener("contextmenu",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top),d=n===-1?void 0:S[n]?.waferIndex;P||d===void 0||!u.onWaferContextMenu||(E.style.display="none",u.onWaferContextMenu(d,T??void 0,t))}),ce?.disconnect(),ce=dt(a,()=>Z()),Z()}$();function Ue(i){ve.set(i)&&(T=i,$())}function Fe(i){ae.set(i)&&$()}return{card:a,setTest:Ue,setAxisPrefs:Fe,destroy:()=>ce?.disconnect()}}
1
+ import{buildTestBoxplotData as rt}from"../../stats/boxplot.js";import{SPACE as He,fontPx as _,FONT as ct,CLR as ft}from"../toolbar.js";import{fmt as ge}from"../../renderer/fmt.js";import{fitTicks as ut}from"../../renderer/axisTicks.js";import{QUANTITY as De}from"./palette.js";import{cardShell as mt,observeResize as dt,makeTooltip as xt,positionChartTooltip as pt,makeBackButton as ht,makeLinkedTestSelect as gt,makeToggle as bt,makeLinkedAxisPrefs as yt,renderEmptyState as Ee,fitRowsHeight as Tt,setChartGrow as wt,resolveChartCanvasColors as vt,makeAxisFormat as Oe,horizontalTickSpacing as kt,resolveAxisRange as Mt,shouldIncludeLimitsByDefault as Lt,drawOffAxisLimits as Ct,limitLabelSide as At,limitLines as We,limitExtent as $t,hasBothLimitKinds as St,stackLabelRows as It,strokeLimitLine as Bt,PADDING as M,VALUE_WIDTH as Rt,WAFER_MENU_HINT as Ht,prepareCanvas as Dt}from"./chartShell.js";import{escHtml as L,maxOf as Et,minOf as Ot}from"../../core/utils.js";const C=24,A=5,Pe=110,Wt=12,_e=20;export function renderBoxplotPanel(u){const{title:ne="Test value distribution",items:Ge,testDefs:be,onSaveImage:Ne,groups:g,groupLabelText:oe="group",onOpen:Q}=u,Xe=u.openActionLabel??"open that wafer",qe=u.openActionLabel??"open this wafer",{card:a,heading:Ye,body:x,controlsRow:U}=mt(ne,Ne,u.ownerDocument);wt(a,"rows"),a.style.flex="0 0 auto",x.style.flex="0 0 auto",a.style.alignSelf="start";const ie=be.filter(i=>i.testNumber!==void 0);let T=u.selectedTestNumber??ie[0]?.testNumber??null,le=!1,F=0,b=null,D=null,se=null,ye=null;function ze(){const i=!g||g.length===0?"":b??"\0overview";if(se&&ye===i)return se;const p=!g||g.length===0?Ge:b!==null?g.find(v=>v.key===b)?.items??[]:g.map(v=>({label:v.key,dies:v.items.flatMap(S=>S.dies??[])}));return se=p,ye=i,p}function Te(){b!==null&&!D?(D=ht(()=>{we(null),u.onGroupChange?.(null)},a.ownerDocument),U.appendChild(D)):b===null&&D&&(D.remove(),D=null),Ye.textContent=b!==null?`${ne} \u2014 ${oe}: ${b}`:ne}function we(i){return b===i||i!==null&&!(g??[]).some(p=>p.key===i)?!1:(b=i,Te(),j(),$(),!0)}const ve=gt(ie,T,i=>{T=i,$(),u.onTestChange?.(i)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Qe=ve.el;U.appendChild(Qe),U.appendChild(bt("Log scale",le,i=>{le=i,$()},a.ownerDocument));const ae=a.ownerDocument.createElement("span");Object.assign(ae.style,{display:"inline-flex",gap:He.lg,alignItems:"center"}),U.appendChild(ae);const re=yt(ae,u.axisPrefs,i=>{u.onAxisPrefsChange?.(i),$()},a.ownerDocument),V=a.ownerDocument.createElement("div");V.dataset.wmapCaption="1",Object.assign(V.style,{color:ft.label,fontSize:ct.body,marginBottom:He.sm}),a.insertBefore(V,x);function j(){const i=!!g&&g.length>0&&b===null,p=[];i?p.push(`click a ${oe}'s box to see it by wafer`):Q&&p.push(`click a box to ${Xe}`);const v=p.length?`${p[0][0].toUpperCase()}${p[0].slice(1)} \xB7 `:"";V.textContent=`${v}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(F?` \xB7 axis clipped, ${F} value${F===1?"":"s"} outside`:"")}j(),Te();const E=xt(a);let ce=null;function $(){const{includeLimits:i,clipOutliers:p,limits:v}=re.get();if(j(),x.innerHTML="",ie.length===0||T===null){Ee(x,"No parametric test data available for box plots.");return}const S=ze(),h=rt(S,T),K=be.find(t=>t.testNumber===T),O=K?.unit,fe=We(K,v),{lo:Ve,hi:je}=$t(fe);if(h.every(t=>t.count===0)){Ee(x,"No parametric test data available for box plots.");return}const m=a.ownerDocument.createElement("canvas");m.style.display="block",m.style.cursor="default",x.appendChild(m);const ke=M*2+Math.min(h.length,Wt)*(C+A)+_e;x.style.maxHeight=`${ke}px`,x.style.overflowY="auto",x.style.scrollbarGutter="stable";let G=-1;const ue=h.filter(t=>t.count>0),Me=Ot(ue.map(t=>t.min)),Le=Et(ue.map(t=>t.max)),Ce=i??Lt(Me,Le,Ve,je);re.sync(Ce,We(K).length>0,St(K));const N=Mt({dataMin:Me,dataMax:Le,limits:fe,includeLimits:Ce,clipOutliers:p,values:ue.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});F=N.clippedCount,j();const W=N.lo,J=N.hi,Ke=J-W||1,Z=le&&W>0,me=Z?Math.log10(W):0,Ae=(Z?Math.log10(J):0)-me||1,de=Math.max(Math.abs(W),Math.abs(J));function Je(){const t=M+Pe,s=m.clientWidth-t-Rt-M;return{plotX:t,plotMaxWidth:Math.max(10,s)}}function I(t,s,n){return Z?s+(Math.log10(t)-me)/Ae*n:s+(t-W)/Ke*n}function Ze(t,s){if(Z){const d=[];for(let e=0;e<=4;e++)d.push(Math.pow(10,me+Ae*e/4));return{ticks:d,axis:Oe(de,O)}}const n=ut(W,J,s,kt(t,de,O));return{ticks:n.ticks,axis:Oe(de,O,n.step||void 0)}}function ee(){const t=vt(a),s=M*2+h.length*(C+A)+_e;x.style.maxHeight=`${Tt(a,x,ke,s)}px`;const n=x.clientWidth,d=Dt(m,a,n,s);if(!d)return;const{ctx:e}=d;e.font=`${_(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:c}=Je();h.forEach((o,f)=>{const y=M+f*(C+A),r=y+C/2;f===G&&(e.fillStyle=t.bgHover,e.fillRect(0,y-A/2,n,C+A)),e.fillStyle=t.text,e.textAlign="right";const te=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(te,M+Pe-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const it=l+c,q=at=>Math.min(it,Math.max(l,at)),Ie=I(o.min,l,c),Be=I(o.max,l,c),B=q(Ie),Y=q(I(o.q1,l,c)),Re=q(I(o.median,l,c)),he=q(I(o.q3,l,c)),R=q(Be),lt=Ie<B,st=Be>R,H=y+3,z=y+C-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(B,r),e.lineTo(Y,r),e.moveTo(he,r),e.lineTo(R,r);const k=3;lt?(e.moveTo(B+k,r-k),e.lineTo(B,r),e.lineTo(B+k,r+k)):(e.moveTo(B,H),e.lineTo(B,z)),st?(e.moveTo(R-k,r-k),e.lineTo(R,r),e.lineTo(R-k,r+k)):(e.moveTo(R,H),e.lineTo(R,z)),e.stroke(),e.fillStyle=De,e.globalAlpha=.35,e.fillRect(Y,H,Math.max(1,he-Y),z-H),e.globalAlpha=1,e.strokeStyle=De,e.strokeRect(Y,H,Math.max(1,he-Y),z-H),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Re,H),e.lineTo(Re,z),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(ge(o.median,O,"engineering"),l+c+10,r)});const w=M+h.length*(C+A);e.font=`${_(-1)}px system-ui, sans-serif`,Ct(e,N.offAxis,{left:l,right:l+c,top:0,bottom:w},"horizontal",t.limitLine,o=>ge(o,O,"engineering"));const et=new Set(N.offAxis.map(o=>o.value)),Se=fe.filter(o=>!et.has(o.value)).map(o=>{const f=I(o.value,l,c),y=e.measureText(o.label).width,r=At(f,y,l,l+c,o.end==="lo"),te=r<0?f-3-y:f+3;return{l:o,x:f,dir:r,start:te,end:te+y}}),tt=It(Se),nt=_(-1)+2;Se.forEach(({l:o,x:f,dir:y},r)=>{Bt(e,o,t.limitLine,f,0,f,w),e.fillStyle=t.limitLine,e.textAlign=y<0?"right":"left",e.textBaseline="bottom",e.fillText(o.label,f+y*3,w-1-tt[r]*nt)}),e.font=`${_(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,w),e.lineTo(l+c,w),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:ot,axis:pe}=Ze(e,c);for(const o of ot){const f=I(o,l,c);e.beginPath(),e.moveTo(f,w),e.lineTo(f,w+4),e.stroke(),e.fillText(pe.tick(o),f,w+6)}pe.unitLabel&&(e.textAlign="left",e.font=`${_(-1)}px system-ui, sans-serif`,e.fillText(`(${pe.unitLabel})`,l+c+22,w+6),e.font=`${_(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function xe(t){const s=Math.floor((t-M+A/2)/(C+A));return s>=0&&s<h.length?s:-1}function X(t){return ge(t,O,"engineering")}const P=!!g&&g.length>0&&b===null,$e=t=>!P&&!!Q&&S[t]?.waferIndex!==void 0;m.addEventListener("mousemove",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top),d=n>=0&&h[n].count>0&&(P||$e(n));if(n!==G&&(G=n,m.style.cursor=d?"pointer":"default",ee()),n>=0&&h[n].count>0){const e=h[n],l=P?`<br><em>click to see this ${L(oe)} by wafer</em>`:$e(n)?`<br><em>click to ${L(qe)}</em>`:"",c=!P&&u.onWaferContextMenu&&S[n]?.waferIndex!==void 0?Ht:"";E.innerHTML=`<strong>${L(e.label)}</strong> (${e.count} dies)<br>max ${L(X(e.max))}<br>q3 ${L(X(e.q3))}<br>median ${L(X(e.median))}<br>q1 ${L(X(e.q1))}<br>min ${L(X(e.min))}${l}${c}`,E.style.display="block",pt(E,a,t.clientX,t.clientY)}else E.style.display="none"}),m.addEventListener("mouseleave",()=>{G!==-1&&(G=-1,ee()),E.style.display="none"}),m.addEventListener("click",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top);if(n===-1||h[n].count===0)return;if(P){const e=h[n].label;we(e),u.onGroupChange?.(e);return}const d=S[n]?.waferIndex;Q&&d!==void 0&&T!==null&&Q(d,T)}),m.addEventListener("contextmenu",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top),d=n===-1?void 0:S[n]?.waferIndex;P||d===void 0||!u.onWaferContextMenu||(E.style.display="none",u.onWaferContextMenu(d,T??void 0,t))}),ce?.disconnect(),ce=dt(a,()=>ee()),ee()}$();function Ue(i){ve.set(i)&&(T=i,$())}function Fe(i){re.set(i)&&$()}return{card:a,setTest:Ue,setAxisPrefs:Fe,destroy:()=>ce?.disconnect()}}
@@ -1 +1 @@
1
- import{buildCapabilityData as ve}from"../../stats/capability.js";import{capabilityColor as U}from"./palette.js";import{LEADING as Se,SPACE as T,fontPx as J,FONT as Z,CLR as _}from"../toolbar.js";import{cardShell as Me,isExpandedCard as Te,chartFillHeight as $e,applyCanvasFlow as Ce,observeResize as Le,makeTooltip as ke,positionChartTooltip as Pe,renderEmptyState as Ee,resolveChartCanvasColors as De,chartSwatchCss as Ae,prepareCanvas as We,chartDpr as we}from"./chartShell.js";import{fmt as Ne}from"../../renderer/fmt.js";import{escHtml as V}from"../../core/utils.js";import{DERIVED_MARK as ee,DERIVED_KEY as le}from"../../renderer/testLabel.js";function N(v){return v==="spec"?{lo:"LSL",hi:"USL",noun:"spec limits"}:{lo:"Lo limit",hi:"Hi limit",noun:"test limits"}}function ae(v){let D;for(const z of v){if(!z.hasSpec)continue;const A=z.limitBasis??"test";if(D===void 0)D=A;else if(D!==A)return"mixed"}return D}const Ie=30,re=160,ce=90,me=28,C=42;function j(v){return v===null?"\u2014":v.toFixed(2)}export function renderCapabilityPanel(v){const{title:D="Process capability",items:z,testDefs:A,onSaveImage:de,onSelectTest:X}=v;let q=v.selectedTestNumber??null;const{card:m,body:L}=Me(D,de,v.ownerDocument),pe=A.some(t=>t.testNumber!==void 0);L.style.overflowX="auto";function he(){return z}const W=m.ownerDocument.createElement("div");Object.assign(W.style,{display:"flex",flexDirection:"column",gap:T.xs,marginBottom:T.sm}),m.insertBefore(W,L);const I=ke(m);let F=()=>{};function fe(t,l,S,f,u){W.innerHTML="";const $=m.ownerDocument.createElement("span");Object.assign($.style,{display:"inline-flex",alignItems:"center",gap:T.sm,color:_.value,fontSize:Z.body,fontWeight:"500"});const d=S-t,y=l>0?` \xB7 ${l} without both limits`:"",M=m.ownerDocument.createElement("span");M.textContent=d>0?`${t} of ${S} tests shown${y} \xB7 ${d} excluded (no recorded values)`:`${t} test${t!==1?"s":""} shown${y}`,$.appendChild(M),W.appendChild($);const k=l>=t,b=m.ownerDocument.createElement("div");b.textContent=k?"No test has both 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.":(u==="mixed"?"Normalised to each test's spec limits (LSL = 0, USL = 1), or its test limits (Lo limit = 0, Hi limit = 1) where it has no spec limits, worst Ppk first. ":`Normalised to ${N(u).noun} (${N(u).lo} = 0, ${N(u).hi} = 1), worst Ppk first. `)+"Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(b.style,{color:_.label,fontSize:Z.body,lineHeight:Se.base,marginTop:T.xs,maxWidth:"78ch"}),W.appendChild(b),W.appendChild(ue(m.ownerDocument,l>0,k,f>0))}function ue(t,l,S=!1,f=!1){const u=t.createElement("div");Object.assign(u.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${T.xs} ${T.xl}`,marginTop:T.xl,marginBottom:T.xs,fontSize:Z.body,color:_.label});const $=S?[]:[{color:U(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:U(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:U(.5),label:"Poor \xB7 Ppk < 1.0"}];l&&$.push({color:_.label,label:"No limits \xB7 no Ppk",dashed:!0});for(const d of $){const y=t.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:T.sm});const M=t.createElement("span");M.style.cssText=Ae(d.color,d.dashed?"outline":"solid");const k=t.createElement("span");k.textContent=d.label,y.append(M,k),u.appendChild(y)}if(f){const d=t.createElement("span");Object.assign(d.style,{display:"inline-flex",alignItems:"center",gap:T.sm});const y=t.createElement("span");y.textContent=ee,Object.assign(y.style,{color:_.value,fontWeight:"600"});const M=t.createElement("span");M.textContent=le,d.append(y,M),u.appendChild(d)}return u}function be(t){if(L.innerHTML="",t.length===0)return Ee(L,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const l=m.ownerDocument.createElement("canvas");l.style.display="block",l.style.cursor="default",L.appendChild(l);const S=t.length,f=Math.min(0,...t.map(n=>n.min)),u=Math.max(1,...t.map(n=>n.max)),$=(u-f)*.05||.1,d=f-$,M=u+$-d||1;function k(n){const o=Math.floor(n/S);return Math.max(Ie,Te(m)?o:Math.min(re,o))}function b(n,o,i){return o+(1-(n-d)/M)*i}function ge(n,o,i,s,P){const x=t.filter(g=>g.hasSpec).length,w=x===t.length,p=x===0,E=ae(t),e=E==="mixed"?{lo:"Lo",hi:"Hi",noun:"limits"}:N(E),h=p?"max":E==="spec"?e.hi:"Hi",a=p?"min":E==="spec"?e.lo:"Lo",H=w?`normalised to ${e.noun}`:p?"normalised to range":"normalised (per test)";n.font=`${J(-1)}px system-ui, sans-serif`,n.textAlign="right",n.textBaseline="middle";for(const[g,r]of[[1,h],[0,a]]){const c=b(g,i,s);c<i-1||c>i+s+1||(n.strokeStyle=o.limitLine,n.lineWidth=1,n.beginPath(),n.moveTo(C-5,c),n.lineTo(C,c),n.stroke(),n.fillStyle=o.textMuted,n.fillText(r,C-8,c))}n.save(),n.translate(9,i+s/2),n.rotate(-Math.PI/2),n.textAlign="center",n.fillStyle=o.textMuted,n.fillText(H,0,0),n.restore()}let B=-1;function G(){Ce(l);const n=L.clientWidth,o=k(n-C),i=o*S,s=me,P=C+i,x=$e(m,L,l,me+200+ce),w=Math.max(s+60,x-ce),p=w-s,E=We(l,m,P,x);if(!E)return;const{ctx:e}=E,h=De(m);t.forEach((a,H)=>{const g=C+H*o,r=g+o/2,c=Math.max(4,Math.min(o,re)*.55);if(H===B&&(e.fillStyle=h.bgHover,e.fillRect(g,s,o,w-s)),t[H].testNumber===q&&(e.fillStyle=h.accent,e.fillRect(g,w-2,o,2)),a.hasSpec){const oe=b(0,s,p),se=b(1,s,p);e.strokeStyle=h.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(g,oe),e.lineTo(g+o,oe),e.moveTo(g,se),e.lineTo(g+o,se),e.stroke(),e.setLineDash([])}const K=a.hasSpec?U(a.ppk):h.textMuted,Q=b(a.min,s,p),R=b(a.q1,s,p),ne=b(a.median,s,p),O=b(a.q3,s,p),Y=b(a.max,s,p);e.strokeStyle=h.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(r,Q),e.lineTo(r,R),e.moveTo(r,O),e.lineTo(r,Y),e.moveTo(r-c/4,Q),e.lineTo(r+c/4,Q),e.moveTo(r-c/4,Y),e.lineTo(r+c/4,Y),e.stroke(),e.fillStyle=K,e.globalAlpha=.5,e.fillRect(r-c/2,Math.min(R,O),c,Math.max(1,Math.abs(O-R))),e.globalAlpha=1,e.strokeStyle=K,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(r-c/2,Math.min(R,O),c,Math.max(1,Math.abs(O-R))),e.setLineDash([]),e.strokeStyle=h.text,e.lineWidth=2,e.beginPath(),e.moveTo(r-c/2,ne),e.lineTo(r+c/2,ne),e.stroke(),e.lineWidth=1,e.font=`${J(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?K:h.textMuted,e.fillText(a.hasSpec?j(a.ppk):"\u2014",r,s-8);const ie=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(r,w+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${J(-1)}px system-ui, sans-serif`,e.fillStyle=h.textMuted,e.fillText(ie,0,0),a.derived&&(e.fillStyle=h.text,e.fillText(`${ee} `,-e.measureText(ie).width,0)),e.restore()}),e.strokeStyle=h.border,e.lineWidth=1,e.strokeRect(C,s,i,p),ge(e,h,s,p,i)}function te(n){const o=l.getBoundingClientRect(),i=L.clientWidth,s=k(i-C),P=(n.clientX-o.left)*(l.width/we(l)/o.width)-C,x=Math.floor(P/s);return x>=0&&x<S?x:-1}return l.addEventListener("mousemove",n=>{const o=te(n);if(o!==B&&(B=o,G()),o===-1){I.style.display="none";return}const i=t[o],s=x=>V(Ne(x,i.unit)),P=i.derived?`<em>${ee} ${V(le)}</em>`+(i.expression?`<br><code>${V(i.expression)}</code>`:"")+"<br>":"";I.innerHTML=`<strong>${V(i.label)}</strong> (n=${i.n})<br>${P}`+(i.hasSpec?`${N(i.limitBasis).lo} ${s(i.lsl)} \xB7 ${N(i.limitBasis).hi} ${s(i.usl)}<br>mean ${s(i.mean)}<br>Cp ${j(i.cp)} \xB7 Cpk ${j(i.cpk)}<br>Pp ${j(i.pp)} \xB7 Ppk ${j(i.ppk)}`:`<em>No limits \u2014 sorted by variability</em><br>mean ${s(i.mean)} \xB7 stddev ${s(i.stdOverall)}`)+(X?"<br><em>click to view in boxplot</em>":""),I.style.display="block",Pe(I,m,n.clientX,n.clientY),l.style.cursor=X?"pointer":"default"}),l.addEventListener("mouseleave",()=>{B!==-1&&(B=-1,G()),I.style.display="none"}),l.addEventListener("click",n=>{if(!X)return;const o=te(n);o!==-1&&X(t[o].testNumber)}),G}function ye(){const t=ve(he(),A),l=A.filter(f=>f.testNumber!==void 0).length,S=t.filter(f=>!f.hasSpec).length;fe(t.length,S,l,t.filter(f=>f.derived).length,ae(t)),F=be(t),F()}const xe=Le(m,()=>F());return ye(),{card:m,hasData:pe,setTest:t=>{q!==t&&(q=t,F())},destroy:()=>xe.disconnect()}}
1
+ import{buildCapabilityData as ve}from"../../stats/capability.js";import{capabilityColor as U}from"./palette.js";import{LEADING as Se,SPACE as T,fontPx as J,FONT as Z,CLR as _}from"../toolbar.js";import{cardShell as Me,isExpandedCard as Te,chartFillHeight as $e,applyCanvasFlow as Ce,observeResize as Le,makeTooltip as ke,positionChartTooltip as Pe,renderEmptyState as Ee,resolveChartCanvasColors as we,chartSwatchCss as De,prepareCanvas as Ae,chartDpr as We}from"./chartShell.js";import{fmt as Ne}from"../../renderer/fmt.js";import{escHtml as V}from"../../core/utils.js";import{DERIVED_MARK as ee,DERIVED_KEY as le}from"../../renderer/testLabel.js";function N(S){return S==="spec"?{lo:"LSL",hi:"USL",noun:"spec limits"}:{lo:"Lo limit",hi:"Hi limit",noun:"test limits"}}function ae(S){let w;for(const z of S){if(!z.hasSpec)continue;const D=z.limitBasis??"test";if(w===void 0)w=D;else if(w!==D)return"mixed"}return w}const Ie=30,re=160,ce=90,me=28,C=42;function j(S){return S===null?"\u2014":S.toFixed(2)}export function renderCapabilityPanel(S){const{title:w="Process capability",items:z,testDefs:D,onSaveImage:de,onSelectTest:X}=S;let q=S.selectedTestNumber??null;const{card:m,body:L}=Me(w,de,S.ownerDocument),pe=D.some(t=>t.testNumber!==void 0);L.style.overflowX="auto";function he(){return z}const A=m.ownerDocument.createElement("div");Object.assign(A.style,{display:"flex",flexDirection:"column",gap:T.xs,marginBottom:T.sm}),m.insertBefore(A,L);const I=ke(m);let F=()=>{};function fe(t,l,M,u,b){A.innerHTML="";const $=m.ownerDocument.createElement("span");Object.assign($.style,{display:"inline-flex",alignItems:"center",gap:T.sm,color:_.value,fontSize:Z.body,fontWeight:"500"});const d=M-t,y=l>0?` \xB7 ${l} without both limits`:"",x=m.ownerDocument.createElement("span");x.dataset.wmapCaption="1",x.textContent=d>0?`${t} of ${M} tests shown${y} \xB7 ${d} excluded (no recorded values)`:`${t} test${t!==1?"s":""} shown${y}`,$.appendChild(x),A.appendChild($);const k=l>=t,p=m.ownerDocument.createElement("div");p.dataset.wmapCaption="1",p.textContent=k?"No test has both 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.":(b==="mixed"?"Normalised to each test's spec limits (LSL = 0, USL = 1), or its test limits (Lo limit = 0, Hi limit = 1) where it has no spec limits, worst Ppk first. ":`Normalised to ${N(b).noun} (${N(b).lo} = 0, ${N(b).hi} = 1), worst Ppk first. `)+"Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(p.style,{color:_.label,fontSize:Z.body,lineHeight:Se.base,marginTop:T.xs,maxWidth:"78ch"}),A.appendChild(p),A.appendChild(ue(m.ownerDocument,l>0,k,u>0))}function ue(t,l,M=!1,u=!1){const b=t.createElement("div");Object.assign(b.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${T.xs} ${T.xl}`,marginTop:T.xl,marginBottom:T.xs,fontSize:Z.body,color:_.label});const $=M?[]:[{color:U(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:U(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:U(.5),label:"Poor \xB7 Ppk < 1.0"}];l&&$.push({color:_.label,label:"No limits \xB7 no Ppk",dashed:!0});for(const d of $){const y=t.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:T.sm});const x=t.createElement("span");x.style.cssText=De(d.color,d.dashed?"outline":"solid");const k=t.createElement("span");k.textContent=d.label,y.append(x,k),b.appendChild(y)}if(u){const d=t.createElement("span");Object.assign(d.style,{display:"inline-flex",alignItems:"center",gap:T.sm});const y=t.createElement("span");y.textContent=ee,Object.assign(y.style,{color:_.value,fontWeight:"600"});const x=t.createElement("span");x.textContent=le,d.append(y,x),b.appendChild(d)}return b}function be(t){if(L.innerHTML="",t.length===0)return Ee(L,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const l=m.ownerDocument.createElement("canvas");l.style.display="block",l.style.cursor="default",L.appendChild(l);const M=t.length,u=Math.min(0,...t.map(n=>n.min)),b=Math.max(1,...t.map(n=>n.max)),$=(b-u)*.05||.1,d=u-$,x=b+$-d||1;function k(n){const o=Math.floor(n/M);return Math.max(Ie,Te(m)?o:Math.min(re,o))}function p(n,o,i){return o+(1-(n-d)/x)*i}function ge(n,o,i,s,P){const g=t.filter(v=>v.hasSpec).length,W=g===t.length,h=g===0,E=ae(t),e=E==="mixed"?{lo:"Lo",hi:"Hi",noun:"limits"}:N(E),f=h?"max":E==="spec"?e.hi:"Hi",a=h?"min":E==="spec"?e.lo:"Lo",H=W?`normalised to ${e.noun}`:h?"normalised to range":"normalised (per test)";n.font=`${J(-1)}px system-ui, sans-serif`,n.textAlign="right",n.textBaseline="middle";for(const[v,r]of[[1,f],[0,a]]){const c=p(v,i,s);c<i-1||c>i+s+1||(n.strokeStyle=o.limitLine,n.lineWidth=1,n.beginPath(),n.moveTo(C-5,c),n.lineTo(C,c),n.stroke(),n.fillStyle=o.textMuted,n.fillText(r,C-8,c))}n.save(),n.translate(9,i+s/2),n.rotate(-Math.PI/2),n.textAlign="center",n.fillStyle=o.textMuted,n.fillText(H,0,0),n.restore()}let B=-1;function G(){Ce(l);const n=L.clientWidth,o=k(n-C),i=o*M,s=me,P=C+i,g=$e(m,L,l,me+200+ce),W=Math.max(s+60,g-ce),h=W-s,E=Ae(l,m,P,g);if(!E)return;const{ctx:e}=E,f=we(m);t.forEach((a,H)=>{const v=C+H*o,r=v+o/2,c=Math.max(4,Math.min(o,re)*.55);if(H===B&&(e.fillStyle=f.bgHover,e.fillRect(v,s,o,W-s)),t[H].testNumber===q&&(e.fillStyle=f.accent,e.fillRect(v,W-2,o,2)),a.hasSpec){const oe=p(0,s,h),se=p(1,s,h);e.strokeStyle=f.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(v,oe),e.lineTo(v+o,oe),e.moveTo(v,se),e.lineTo(v+o,se),e.stroke(),e.setLineDash([])}const K=a.hasSpec?U(a.ppk):f.textMuted,Q=p(a.min,s,h),R=p(a.q1,s,h),ne=p(a.median,s,h),O=p(a.q3,s,h),Y=p(a.max,s,h);e.strokeStyle=f.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(r,Q),e.lineTo(r,R),e.moveTo(r,O),e.lineTo(r,Y),e.moveTo(r-c/4,Q),e.lineTo(r+c/4,Q),e.moveTo(r-c/4,Y),e.lineTo(r+c/4,Y),e.stroke(),e.fillStyle=K,e.globalAlpha=.5,e.fillRect(r-c/2,Math.min(R,O),c,Math.max(1,Math.abs(O-R))),e.globalAlpha=1,e.strokeStyle=K,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(r-c/2,Math.min(R,O),c,Math.max(1,Math.abs(O-R))),e.setLineDash([]),e.strokeStyle=f.text,e.lineWidth=2,e.beginPath(),e.moveTo(r-c/2,ne),e.lineTo(r+c/2,ne),e.stroke(),e.lineWidth=1,e.font=`${J(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?K:f.textMuted,e.fillText(a.hasSpec?j(a.ppk):"\u2014",r,s-8);const ie=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(r,W+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${J(-1)}px system-ui, sans-serif`,e.fillStyle=f.textMuted,e.fillText(ie,0,0),a.derived&&(e.fillStyle=f.text,e.fillText(`${ee} `,-e.measureText(ie).width,0)),e.restore()}),e.strokeStyle=f.border,e.lineWidth=1,e.strokeRect(C,s,i,h),ge(e,f,s,h,i)}function te(n){const o=l.getBoundingClientRect(),i=L.clientWidth,s=k(i-C),P=(n.clientX-o.left)*(l.width/We(l)/o.width)-C,g=Math.floor(P/s);return g>=0&&g<M?g:-1}return l.addEventListener("mousemove",n=>{const o=te(n);if(o!==B&&(B=o,G()),o===-1){I.style.display="none";return}const i=t[o],s=g=>V(Ne(g,i.unit)),P=i.derived?`<em>${ee} ${V(le)}</em>`+(i.expression?`<br><code>${V(i.expression)}</code>`:"")+"<br>":"";I.innerHTML=`<strong>${V(i.label)}</strong> (n=${i.n})<br>${P}`+(i.hasSpec?`${N(i.limitBasis).lo} ${s(i.lsl)} \xB7 ${N(i.limitBasis).hi} ${s(i.usl)}<br>mean ${s(i.mean)}<br>Cp ${j(i.cp)} \xB7 Cpk ${j(i.cpk)}<br>Pp ${j(i.pp)} \xB7 Ppk ${j(i.ppk)}`:`<em>No limits \u2014 sorted by variability</em><br>mean ${s(i.mean)} \xB7 stddev ${s(i.stdOverall)}`)+(X?"<br><em>click to view in boxplot</em>":""),I.style.display="block",Pe(I,m,n.clientX,n.clientY),l.style.cursor=X?"pointer":"default"}),l.addEventListener("mouseleave",()=>{B!==-1&&(B=-1,G()),I.style.display="none"}),l.addEventListener("click",n=>{if(!X)return;const o=te(n);o!==-1&&X(t[o].testNumber)}),G}function ye(){const t=ve(he(),D),l=D.filter(u=>u.testNumber!==void 0).length,M=t.filter(u=>!u.hasSpec).length;fe(t.length,M,l,t.filter(u=>u.derived).length,ae(t)),F=be(t),F()}const xe=Le(m,()=>F());return ye(),{card:m,hasData:pe,setTest:t=>{q!==t&&(q=t,F())},destroy:()=>xe.disconnect()}}
@@ -1,4 +1,5 @@
1
- import { type SaveImageHandler } from '../toolbar.js';
1
+ import { type OverlayHandle, type SaveImageHandler } from '../toolbar.js';
2
+ import { robustFence } from '../../stats/math.js';
2
3
  export type { SaveImageHandler };
3
4
  /**
4
5
  * The card's own expand button is hidden for the modal's duration and shown
@@ -18,7 +19,7 @@ export type { SaveImageHandler };
18
19
  export declare function openChartExpandModal(card: HTMLElement, title: string, opts?: {
19
20
  anchor?: Element;
20
21
  onClosed?: () => void;
21
- }): void;
22
+ }): OverlayHandle | null;
22
23
  export interface ChartCanvasColors {
23
24
  text: string;
24
25
  textMuted: string;
@@ -234,12 +235,60 @@ export declare const VERTICAL_TICK_SPACING_PX = 28;
234
235
  export declare function horizontalTickSpacing(ctx: CanvasRenderingContext2D, vRef: number, unit: string | undefined): (step: number, ticks: number[]) => number;
235
236
  /** Draw a small "(unit)" label at (x, y), restoring the context's text state afterward. */
236
237
  export declare function drawAxisUnit(ctx: CanvasRenderingContext2D, unit: string, x: number, y: number, color: string): void;
237
- export declare function saveCanvasPng(canvas: HTMLCanvasElement, filenameStem: string, onSaveImage?: SaveImageHandler): void;
238
+ /** Text for a saved image or a printed page: what a caption says about the chart, without telling the reader to click. */
239
+ export declare function captionText(text: string): string;
240
+ export interface CardContext {
241
+ controls: string[];
242
+ captions: string[];
243
+ legend: Array<{
244
+ label: string;
245
+ color: string;
246
+ }>;
247
+ }
248
+ /**
249
+ * Reads what a card currently says around its canvas: its controls' settings ("Test: vth_n_mV · 1001", "Sort: yield"),
250
+ * its caption elements (marked `data-wmap-caption`, with instructions to click or drag removed), and its legend.
251
+ */
252
+ export declare function describeCard(card: HTMLElement): CardContext;
253
+ /** What a saved chart carries besides its canvas: the legend and the statement of what it shows are DOM, not canvas. */
254
+ export interface PngDecor {
255
+ /** Which test, wafer, group or sort the card's controls are set to ("Test: vth_n_mV · 1001"): a chart saved without them is unlabelled. */
256
+ controls?: readonly string[];
257
+ /** The population and what was combined or left out, under the title: a line, or several. */
258
+ caption?: string | readonly string[];
259
+ /** The colour key of a chart coloured by category. */
260
+ legend?: ReadonlyArray<{
261
+ label: string;
262
+ color: string;
263
+ }>;
264
+ /** The colour key of a chart coloured on a gradient. */
265
+ colorbar?: {
266
+ label: string;
267
+ lo: string;
268
+ hi: string;
269
+ color: (t: number) => string;
270
+ };
271
+ }
272
+ export interface PngHeader {
273
+ /** The card the chart lives in: its colours are the saved image's, so a dark theme saves a legible dark image. */
274
+ card: HTMLElement;
275
+ title: string;
276
+ decor?: PngDecor;
277
+ }
278
+ /**
279
+ * The canvas as it is saved: under a header that names the chart, states what it shows, and carries the legend.
280
+ * A bare canvas saved with none of these is a picture of unlabelled marks: the title, the population ("6 wafers · 3,966
281
+ * dies"), and the key to the colours live in the card around the canvas, not in it.
282
+ */
283
+ export declare function composeChartPng(canvas: HTMLCanvasElement, header?: PngHeader): HTMLCanvasElement;
284
+ export declare function saveCanvasPng(canvas: HTMLCanvasElement, filenameStem: string, onSaveImage?: SaveImageHandler, header?: PngHeader): void;
238
285
  export interface CardShell {
239
286
  card: HTMLElement;
240
287
  heading: HTMLElement;
241
288
  controlsRow: HTMLElement;
242
289
  body: HTMLElement;
290
+ /** What the card's Save as PNG adds under the title, besides the canvas (a caption, a legend). Read at the moment of saving. */
291
+ setPngDecor(read: () => PngDecor | undefined): void;
243
292
  }
244
293
  /**
245
294
  * THE colour chip that stands for a data series, everywhere in the chart suite.
@@ -303,6 +352,10 @@ export declare function renderEmptyState(body: HTMLElement, message: string, sty
303
352
  export interface ListSelectOption {
304
353
  value: string;
305
354
  label: string;
355
+ /** A heading shown above the first option of each run with the same group. The list stays one flat, searchable list. */
356
+ group?: string;
357
+ /** Why this option cannot be chosen now. It stays in the list, dimmed, with the reason after it, and does nothing when picked. */
358
+ disabled?: string;
306
359
  }
307
360
  /**
308
361
  * A themed single-select picker: a trigger button plus an on-demand popup
@@ -571,18 +624,7 @@ export declare function fillTextOnHalo(ctx: CanvasRenderingContext2D, text: stri
571
624
  export declare function strokeLimitLine(ctx: CanvasRenderingContext2D, line: LimitLine, color: string, x0: number, y0: number, x1: number, y1: number): void;
572
625
  /** The "Limits:" dropdown, offered only where a test has both kinds. */
573
626
  export declare function makeLimitsSelect(current: LimitsShown, onChange: (v: LimitsShown) => void, ownerDocument?: Document): HTMLLabelElement;
574
- /** Robust outlier fence over a value list: Tukey's `Q1 − k·IQR … Q3 + k·IQR`.
575
- *
576
- * Deliberately NOT mean ± 3σ. σ is computed FROM the data including the
577
- * outlier, so a single reading of 1e30 inflates σ far enough that the fence no
578
- * longer excludes it — the classic masking failure, and it fails hardest exactly
579
- * when the outlier is worst. Quartiles are unmoved by the extreme tail.
580
- *
581
- * Returns null when there are too few values for quartiles to mean anything. */
582
- export declare function robustFence(values: number[], k?: number): {
583
- lo: number;
584
- hi: number;
585
- } | null;
627
+ export { robustFence };
586
628
  /**
587
629
  * Whether a panel should include the spec limits in its axis BY DEFAULT.
588
630
  *
@@ -1 +1 @@
1
- import{SHADOW as D,LEADING as j,wireControlHover as L,controlStyle as I,SPACE as m,RADIUS as C,fontPx as k,FONT as S,CLR as f,Z_BASE as X,menuLayerFor as K,wireListNavigation as Y,MENU_SEARCH_THRESHOLD as W,makeMenuSearchBox as Z,markMenuTrigger as R,saveImageBlob as J,openReparentedModal as Q}from"../toolbar.js";import{ICONS as _}from"../icons.js";import{minOf as ee,maxOf as te}from"../../core/utils.js";import{fiveNumberSummary as ne}from"../../stats/math.js";import{fmt as oe,fmtColorbarAxis as ie}from"../../renderer/fmt.js";import{markedTestLabel as re,DERIVED_LANE_PAD as le}from"../../renderer/testLabel.js";export function openChartExpandModal(e,o,n={}){for(const s of e.querySelectorAll('div[style*="position: absolute"]'))s.style.display==="block"&&s.style.pointerEvents==="none"&&(s.style.display="none");const t=e.querySelector("[data-wmap-chart-expand]"),i=e.getAttribute("style")??"",r=e.dataset.wmapChartGrow??"plot";Object.assign(e.style,{flex:"1",minHeight:"0",alignSelf:"stretch",border:"none",borderRadius:"0"}),e.dataset.wmapExpanded="1";const l=Q([e],{title:o,anchor:n.anchor,ownerDocument:e.ownerDocument,boxSize:se[r],onClosed:()=>{delete e.dataset.wmapExpanded,e.setAttribute("style",i),t&&(t.style.display="flex"),n.onClosed?.()}});if(!l){delete e.dataset.wmapExpanded,e.setAttribute("style",i);return}if(t&&(t.style.display="none"),r==="rows"){const s=()=>{const a=Number(e.dataset.wmapChartFullH);if(!a)return;const p=l.box.offsetHeight-l.contentWrap.clientHeight;l.setNaturalHeight(`min(90vh, ${Math.ceil(p+a)}px)`)};s(),(e.ownerDocument.defaultView??window).requestAnimationFrame(s)}}const se={plot:{width:"min(94vw, 1100px)",height:"min(90vh, 760px)"},rows:{width:"min(94vw, 960px)",height:"min(90vh, 700px)"},square:{width:"min(94vw, 820px)",height:"min(90vh, 860px)"}};export const GRID_ALPHA=.4;export function withAlpha(e,o){const n=e.trim();let t,i,r,l=1;const s=n.match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i),a=n.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i);if(s){const p=s[1].length===3?s[1].split("").map(c=>c+c).join(""):s[1];[t,i,r]=[0,2,4].map(c=>parseInt(p.slice(c,c+2),16)),p.length===8&&(l=parseInt(p.slice(6,8),16)/255)}else if(a)[t,i,r]=[a[1],a[2],a[3]].map(Number),a[4]!==void 0&&(l=a[4].endsWith("%")?parseFloat(a[4])/100:Number(a[4]));else return e;return`rgba(${t}, ${i}, ${r}, ${+(l*o).toFixed(3)})`}const ae={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040",accent:"#1a66cc",limitLine:"#b45309"},P={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border",accent:"icon-active",limitLine:"warn-text"};export function resolveChartCanvasColors(e){const o=getComputedStyle(e),n={};for(const t of Object.keys(P)){const i=o.getPropertyValue(`--wmap-${P[t]}`).trim();n[t]=i||ae[t]}return n.grid=withAlpha(n.border,GRID_ALPHA),n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,o){const n=e.ownerDocument.defaultView??window;let t=0;const i=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,o()}))});return i.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,i.disconnect()}}}export function growCardToFitContent(e,o,n){const t=`${H(e,o)+n}px`;e.style.minHeight!==t&&(e.style.minHeight=t)}function H(e,o){const n=e.ownerDocument.defaultView??window,t=n.getComputedStyle(e);let i=0;for(const r of Array.from(e.children)){if(r===o)continue;const l=r,s=n.getComputedStyle(l);s.display==="none"||s.position==="absolute"||s.position==="fixed"||(i+=l.offsetHeight+(parseFloat(s.marginTop)||0)+(parseFloat(s.marginBottom)||0))}return t.boxSizing==="border-box"&&(i+=(parseFloat(t.paddingTop)||0)+(parseFloat(t.paddingBottom)||0)+(parseFloat(t.borderTopWidth)||0)+(parseFloat(t.borderBottomWidth)||0)),i}function z(e,o){const n=e.ownerDocument.defaultView??window;let t=0;for(const i of Array.from(e.children)){if(i===o)continue;const r=i,l=n.getComputedStyle(r);l.display==="none"||l.position==="absolute"||l.position==="fixed"||(t+=r.offsetHeight+(parseFloat(l.marginTop)||0)+(parseFloat(l.marginBottom)||0))}return t}export const WAFER_MENU_HINT="<br><em>right-click to chart this wafer</em>";export function populationPhrase(e,o,n){const t=e.toLocaleString(),i=e===1?"die":"dies";return n?e===o?`${t} ${i} ${n}`:`${t} of ${o.toLocaleString()} dies ${n} (partial and edge-excluded dies left out)`:`${t} ${i}`}export function setChartGrow(e,o){e.dataset.wmapChartGrow=o}export function isExpandedCard(e){return e.dataset.wmapExpanded==="1"}export function bodyRoom(e,o,n){const i=(e.ownerDocument.defaultView??window).getComputedStyle(e),r=e.clientHeight-(parseFloat(i.paddingTop)||0)-(parseFloat(i.paddingBottom)||0),l=i.boxSizing==="border-box"?(parseFloat(i.paddingTop)||0)+(parseFloat(i.paddingBottom)||0)+(parseFloat(i.borderTopWidth)||0)+(parseFloat(i.borderBottomWidth)||0):0;return r-(H(e,o)-l)-(n?z(o,n):0)}export function fitRowsHeight(e,o,n,t,i=0){if(growCardToFitContent(e,o,i+n),e.dataset.wmapChartFullH=String(Math.ceil(H(e,o)+i+t)),!isExpandedCard(e))return n;const r=bodyRoom(e,o)-i;return Math.max(n,Math.min(t,Math.floor(r)))}export function chartFillHeight(e,o,n,t){const i=z(o,n);return growCardToFitContent(e,o,i+t),Math.max(t,o.clientHeight-i)}export function chartDpr(e){return(e.ownerDocument.defaultView??window).devicePixelRatio||1}export function prepareCanvas(e,o,n,t){const i=chartDpr(e);e.width=Math.max(1,Math.floor(n*i)),e.height=Math.max(1,Math.floor(t*i)),e.style.width=`${n}px`,e.style.height=`${t}px`;const r=e.getContext("2d");return r?(r.setTransform(i,0,0,i,0,0),r.clearRect(0,0,n,t),r.font=`${k(-1)}px system-ui, sans-serif`,r.textBaseline="middle",{ctx:r,theme:resolveChartCanvasColors(o)}):null}export function applyCanvasFlow(e,o=0){let n;if(typeof o=="number")n=o;else{const t=(o.ownerDocument.defaultView??window).getComputedStyle(o);n=o.offsetTop+o.offsetHeight+(parseFloat(t.marginBottom)||0)}e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${n}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:oe(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,o,n){if(!o&&n===void 0)return{tick:formatValue,tickWithUnit:formatValue,unitLabel:""};const{tickFmt:t,axisLabel:i,scaledUnit:r}=ie(e,null,o,"engineering",n);return{tick:t,tickWithUnit:r?l=>`${t(l)} ${r}`:t,unitLabel:i}}const ce=8;export const VERTICAL_TICK_SPACING_PX=28;export function horizontalTickSpacing(e,o,n){return(t,i)=>{const r=makeAxisFormat(o,n,t).tick;return i.reduce((l,s)=>Math.max(l,e.measureText(r(s)).width),0)+ce}}export function drawAxisUnit(e,o,n,t,i){const r={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font=`${k(-1)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="middle",e.fillStyle=i,e.fillText(`(${o})`,n,t),Object.assign(e,r)}export function saveCanvasPng(e,o,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const i=t.getContext("2d");i.fillStyle="#ffffff",i.fillRect(0,0,t.width,t.height),i.drawImage(e,0,0),t.toBlob(r=>{r&&J(r,o,n)},"image/png")}const V="10px";export function chartSwatchCss(e,o="solid"){return["display:inline-block",`width:${V}`,`height:${V}`,`border-radius:${C.pill}`,`background:${o==="outline"?"transparent":e}`,`border:1px ${o==="outline"?"dashed":"solid"} ${e}`,"box-sizing:border-box","flex-shrink:0"].join(";")}const de="0.35";export function makeSeriesLegendItem(e,o,n=document){const t=n.createElement("button");t.type="button",Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:m.xs,padding:`${m.xs} ${m.md}`,borderRadius:C.pill,border:`1px solid ${f.menuBorder}`,background:"none",cursor:"pointer",fontSize:S.body,color:f.text,whiteSpace:"nowrap"}),L(t,"bare");const i=n.createElement("span");return i.style.cssText=chartSwatchCss(o),t.append(i,n.createTextNode(e)),{el:t,setState({selected:r,dimmed:l}){t.style.borderColor=r?f.iconActive:f.menuBorder,t.style.fontWeight=r?"600":"400",t.style.opacity=l?de:"1",t.setAttribute("aria-pressed",r?"true":"false")}}}export function cardFrameStyle(){return{background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:C.container,padding:m.xl,fontVariantNumeric:"tabular-nums"}}export function cardShell(e,o,n=document){const t=n.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",...cardFrameStyle(),minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const i=t.ownerDocument.createElement("div");Object.assign(i.style,{display:"flex",alignItems:"center",gap:m.sm,paddingBottom:"6px",marginBottom:m.md,borderBottom:`1px solid ${f.menuBorder}`});const r=t.ownerDocument.createElement("div");r.textContent=e,Object.assign(r.style,{color:f.value,fontSize:S.heading,fontWeight:"600",flex:"1"}),i.appendChild(r);const l=makeTooltip(t),s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Save as PNG"),s.innerHTML=_.download,Object.assign(s.style,{...I("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:j.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(s),attachChartTip(s,t,l,"Save as PNG"),s.addEventListener("click",()=>{const d=t.querySelector("canvas");d&&saveCanvasPng(d,e,o)}),i.appendChild(s);const a=t.ownerDocument.createElement("button");a.type="button",a.setAttribute("aria-label","Expand"),a.innerHTML=_.expand,Object.assign(a.style,{...I("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:j.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(a),attachChartTip(a,t,l,"Expand"),a.dataset.wmapChartExpand="1",a.addEventListener("click",()=>openChartExpandModal(t,r.textContent??e)),i.appendChild(a),t.appendChild(i);const p=t.ownerDocument.createElement("div");Object.assign(p.style,{display:"flex",gap:m.sm,marginBottom:m.md,flexWrap:"wrap",alignItems:"center"}),t.appendChild(p);const c=t.ownerDocument.createElement("div");return Object.assign(c.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(c),{card:t,heading:r,controlsRow:p,body:c}}export function makeSegmented(e,o,n,t=document,i=!1){const r=t.createElement("div");r.setAttribute("role","radiogroup"),Object.assign(r.style,{display:"inline-flex",border:`1px solid ${f.menuBorder}`,borderRadius:C.control,overflow:"hidden"});const l=`seg-${Math.random().toString(36).slice(2,9)}`,s=[];return e.forEach(([a,p],c)=>{const d=t.createElement("label");Object.assign(d.style,{display:"inline-flex",alignItems:"center",fontSize:S.body,padding:i?"1px 6px":"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:c>0?`1px solid ${f.menuBorder}`:"none"}),L(d,"bare");const u=t.createElement("input");u.type="radio",u.name=l,u.value=a,u.checked=a===o,Object.assign(u.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const w=()=>{d.style.background=u.checked?f.bgActive:f.menuBg,d.style.color=u.checked?f.iconActive:f.text,d.dataset.on=u.checked?"true":"false"};s.push(w),w(),u.addEventListener("change",()=>{if(u.checked){for(const v of s)v();n(a)}}),d.append(u,t.createTextNode(p)),r.appendChild(d)}),r}export function makeBackButton(e,o=document){const n=o.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{...I("outlined"),background:f.menuBg,color:f.label}),L(n),n.addEventListener("click",e),n}export function renderEmptyState(e,o,n){const t=e.ownerDocument.createElement("div");t.textContent=o,Object.assign(t.style,{color:f.label,fontSize:S.body,padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeListSelect(e,o,n,t={}){const{maxWidth:i="200px",ownerDocument:r=document,ariaLabel:l,emptyText:s,searchPlaceholder:a="Filter\u2026",hook:p}=t;let c=o;const d=x=>e.find(O=>O.value===x)?.label??"",u=r.createElement("button");u.type="button",p&&(u.dataset.wmapSelect=p),l&&u.setAttribute("aria-label",l),Object.assign(u.style,{fontSize:S.body,padding:`${m.xxs} ${m.sm}`,background:f.menuBg,color:f.text,border:`1px solid ${f.menuBorder}`,borderRadius:C.control,maxWidth:i,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:m.sm,justifyContent:"space-between"}),L(u),R(u,!1);const w=r.createElement("span");Object.assign(w.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const v=r.createElement("span");v.textContent="\u25BE",v.style.flex="0 0 auto",u.append(w,v);const E=()=>{w.textContent=e.length===0?s??"(none)":d(c)||s||"(none)"};if(E(),e.length===0)return u.disabled=!0,u.style.opacity="0.6",u.style.cursor="default",Object.defineProperty(u,"value",{get:()=>c,set:x=>{c=x,E()}});let b=null;const B=()=>{b&&(b.remove(),b=null,R(u,!1))},$=()=>{B(),u.focus()};function G(){const x=u.getBoundingClientRect(),O=r.defaultView??window,N=Math.max(x.width,220),q=Math.min(x.left,Math.max(4,(O.innerWidth??1/0)-N-4));b=r.createElement("div"),b.setAttribute("role","listbox"),b.setAttribute("aria-label",l??"Options"),Object.assign(b.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${q}px`,background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:C.control,boxShadow:D.menu,zIndex:X,minWidth:`${N}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const T=[],F=()=>T.filter(h=>h.row.style.display!=="none").map(h=>h.row);if(e.length>W){const h=Z(y=>{for(const g of T)g.row.style.display=g.label.includes(y)?"":"none"},a,r);Object.assign(h.style,{position:"sticky",top:"0",zIndex:"1",background:f.menuBg}),h.addEventListener("keydown",y=>{if(y.key==="Escape"){$();return}y.key==="ArrowDown"&&(y.preventDefault(),F()[0]?.focus())}),b.appendChild(h)}for(const h of e){const y=h.value===c,g=r.createElement("div");g.textContent=h.label,g.setAttribute("role","option"),g.setAttribute("aria-selected",y?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:`${m.sm} ${m.xl}`,fontSize:S.body,cursor:"pointer",color:y?f.iconActive:f.text,fontWeight:y?"700":"400",background:y?f.menuActive:"transparent",whiteSpace:"nowrap"}),g.addEventListener("mouseenter",()=>{y||(g.style.background=f.menuHover)}),g.addEventListener("mouseleave",()=>{y||(g.style.background="transparent")}),g.addEventListener("click",U=>{U.stopPropagation(),c=h.value,E(),$(),n(h.value)}),b.appendChild(g),T.push({row:g,label:h.label.toLowerCase()})}if(Y(b,F,$),K(u).appendChild(b),R(u,!0),e.length<=W){const h=e.findIndex(y=>y.value===c);F()[h>=0?h:0]?.focus()}}u.addEventListener("click",x=>{if(x.stopPropagation(),b){B();return}G()});const M=x=>{if(!u.isConnected){r.removeEventListener("click",M);return}b&&!b.contains(x.target)&&!u.contains(x.target)&&B()};return r.addEventListener("click",M),Object.defineProperty(u,"value",{get:()=>c,set:x=>{c=x,E()}})}export function testOptionLabels(e){const o=e.some(n=>n.derived);return e.map(n=>(o&&!n.derived?le:"")+re(n,n.testNumber))}export function makeTestSelect(e,o,n,t={}){const{maxWidth:i="200px",emptyText:r="No parametric tests",ownerDocument:l=document}=t;return makeListSelect(testOptionLabels(e).map((s,a)=>({value:String(e[a].testNumber),label:s})),o!==null?String(o):"",s=>n(Number(s)),{maxWidth:i,ownerDocument:l,ariaLabel:"Test",emptyText:r,searchPlaceholder:"Filter tests\u2026"})}export function makeLinkedAxisPrefs(e,o,n,t=document){let i=o?.includeLimits,r=o?.clipOutliers??!1,l=o?.limits??"both";const s=()=>n({includeLimits:i,clipOutliers:r,limits:l});return{get:()=>({includeLimits:i,clipOutliers:r,limits:l}),sync(a,p,c=!1){e.innerHTML="",c&&e.appendChild(makeLimitsSelect(l,d=>{l=d,s()},t)),p&&e.appendChild(makeToggle("Axis includes limits",a,d=>{i=d,s()},t)),e.appendChild(makeToggle("Clip outliers",r,d=>{r=d,s()},t))},set(a){const p=a.limits??"both";return a.includeLimits===i&&a.clipOutliers===r&&p===l?!1:(i=a.includeLimits,r=a.clipOutliers,l=p,!0)}}}export function makeLinkedTestSelect(e,o,n,t={}){let i=o;const r=makeTestSelect(e,i,l=>{l!==i&&(i=l,n(l))},t);return{el:r,get:()=>i,set(l){return i===l||!e.some(s=>s.testNumber===l)?!1:(i=l,r.value=String(l),!0)}}}export const ALL_GROUPS="\0all";export function makeLinkedGroupSelect(e,o,n,t,i={}){let r=n;const l=c=>c??ALL_GROUPS,s=c=>c===ALL_GROUPS?null:c,a=makeLabeledSelect(o,[{value:ALL_GROUPS,label:"All groups"},...e.map(c=>({value:c,label:c}))],l(r),c=>{const d=s(c);d!==r&&(r=d,t(d))},{...i,hook:"group-scope"}),p=a.querySelector('[data-wmap-select="group-scope"]');return{el:a,get:()=>r,set(c){return r===c||c!==null&&!e.includes(c)?!1:(r=c,p&&(p.value=l(c)),!0)}}}export function makeWaferSelect(e,o,n,t={}){const{maxWidth:i="160px",allLabel:r="All wafers",ownerDocument:l=document}=t,s="\0all";return makeListSelect([{value:s,label:r},...e.map((a,p)=>({value:String(p),label:a.label??`#${p}`}))],o===null?s:String(o),a=>n(a===s?null:Number(a)),{maxWidth:i,ownerDocument:l,ariaLabel:r,searchPlaceholder:"Filter wafers\u2026"})}export function makeToggle(e,o,n,t=document){const i=t.createElement("label");Object.assign(i.style,{display:"inline-flex",alignItems:"center",gap:m.xs,fontSize:S.body,color:f.label,cursor:"pointer",userSelect:"none"}),L(i,"bare");const r=t.createElement("input");return r.type="checkbox",r.checked=o,r.style.cssText="margin:0;cursor:pointer;",r.addEventListener("change",()=>n(r.checked)),i.append(r,t.createTextNode(e)),i}export function makeLabeledSelect(e,o,n,t,i={}){const{maxWidth:r="160px",hook:l,ownerDocument:s=document}=i,a=s.createElement("label");a.textContent=e,Object.assign(a.style,{color:f.label,fontSize:S.body,display:"flex",alignItems:"center",gap:m.xs});const p=makeListSelect(o,n,t,{maxWidth:r,ownerDocument:s,hook:l,ariaLabel:e.replace(/:\s*$/,"")});return a.appendChild(p),a}export function makeChartGridWrap(e=document){const o=e.createElement("div");return o.dataset.wmapChartGrid="1",Object.assign(o.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:m.lg,flex:"0 0 auto"}),o}export function makeTooltip(e){const o=e.ownerDocument.createElement("div");return Object.assign(o.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:"rgba(30, 32, 40, 0.93)",color:"#f0f0f2",border:"1px solid rgba(255,255,255,0.10)",borderRadius:C.control,padding:`${m.sm} ${m.xl}`,fontSize:S.sub,fontFamily:S.family,maxWidth:"320px",whiteSpace:"normal",overflowWrap:"break-word",boxShadow:D.menu}),e.appendChild(o),o}export function attachChartTip(e,o,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",i=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,o,i.clientX,i.clientY)}),e.addEventListener("mousemove",i=>{n.style.display==="block"&&positionChartTooltip(n,o,i.clientX,i.clientY)}),e.addEventListener("mouseleave",()=>{n.style.display="none"}),e.addEventListener("click",()=>{n.style.display="none"}),e.addEventListener("focus",()=>{n.textContent=t,n.style.display="block";const i=e.getBoundingClientRect();positionChartTooltip(n,o,i.left+i.width/2,i.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,o,n,t){const i=o.getBoundingClientRect(),r=14,l=4;e.style.left="0",e.style.top="0";const s=e.offsetWidth,a=e.offsetHeight;let p=n-i.left+r,c=t-i.top+r;p+s+l>i.width&&(p=n-i.left-r-s),c+a+l>i.height&&(c=t-i.top-r-a),p=Math.max(l,p),c=Math.max(l,c),e.style.left=`${p}px`,e.style.top=`${c}px`}export const LIMIT_LABEL={test:{lo:"Lo limit",hi:"Hi limit"},spec:{lo:"LSL",hi:"USL"}},LIMIT_DASH={test:[3,3],spec:[10,4]},LIMITS_SHOWN_OPTIONS=[{value:"both",label:"Test + spec"},{value:"test",label:"Test limits"},{value:"spec",label:"Spec limits (LSL/USL)"},{value:"none",label:"None"}];function A(e,o){const[n,t]=o==="test"?[e?.limitLow,e?.limitHigh]:[e?.specLow,e?.specHigh],i=[];return n!==void 0&&Number.isFinite(n)&&i.push({value:n,kind:o,end:"lo",label:LIMIT_LABEL[o].lo}),t!==void 0&&Number.isFinite(t)&&i.push({value:t,kind:o,end:"hi",label:LIMIT_LABEL[o].hi}),i}export function hasBothLimitKinds(e){return A(e,"test").length>0&&A(e,"spec").length>0}export function limitLines(e,o="both"){if(o==="none")return[];const n=A(e,"test"),t=A(e,"spec"),i=o==="test"?n:o==="spec"?t:[...n,...t];return i.length?i:[...n,...t]}export function limitExtent(e){if(!e.length)return{};const o=e.map(n=>n.value);return{lo:ee(o),hi:te(o)}}export function stackLabelRows(e,o=4){const n=e.map((r,l)=>({...r,i:l})).sort((r,l)=>r.start-l.start),t=[],i=new Array(e.length).fill(0);for(const r of n){let l=t.findIndex(s=>r.start>=s+o);l===-1?(l=t.length,t.push(r.end)):t[l]=r.end,i[r.i]=l}return i}export function fillTextOnHalo(e,o,n,t,i){if(i){const r=e.measureText(o),l=2,s=e.fillStyle;e.fillStyle=i,e.fillRect(n-r.actualBoundingBoxLeft-l,t-r.actualBoundingBoxAscent-l,r.actualBoundingBoxLeft+r.actualBoundingBoxRight+2*l,r.actualBoundingBoxAscent+r.actualBoundingBoxDescent+2*l),e.fillStyle=s}e.fillText(o,n,t)}export function strokeLimitLine(e,o,n,t,i,r,l){e.save(),e.strokeStyle=n,e.lineWidth=1,e.setLineDash(LIMIT_DASH[o.kind]),e.beginPath(),e.moveTo(t,i),e.lineTo(r,l),e.stroke(),e.restore()}export function makeLimitsSelect(e,o,n=document){return makeLabeledSelect("Limits:",LIMITS_SHOWN_OPTIONS,e,t=>o(t),{maxWidth:"170px",hook:"limits-shown",ownerDocument:n})}export function robustFence(e,o=1.5){const n=Float64Array.from(e.filter(l=>Number.isFinite(l)));if(n.length<8)return null;const{q1:t,q3:i}=ne(n),r=i-t;return r===0?null:{lo:t-o*r,hi:i+o*r}}export function shouldIncludeLimitsByDefault(e,o,n,t,i=1/3){if(n===void 0&&t===void 0||!Number.isFinite(e)||!Number.isFinite(o))return!1;const r=Math.min(e,n??e),s=Math.max(o,t??o)-r;if(s<=0)return!0;const a=o-e;return a===0?!0:a/s>=i}export function resolveAxisRange(e){const{dataMin:o,dataMax:n,limits:t=[],includeLimits:i,clipOutliers:r,values:l}=e;let s=o,a=n,p=0;if(r&&l?.length){const d=robustFence(l);if(d){const u=Math.max(s,d.lo),w=Math.min(a,d.hi);w>u&&(p=l.filter(v=>Number.isFinite(v)&&(v<u||v>w)).length,s=u,a=w)}}if(i)for(const d of t)s=Math.min(s,d.value),a=Math.max(a,d.value);s===a&&(s-=1,a+=1);const c=[];for(const d of t)d.value<s?c.push({value:d.value,label:d.label,side:"lo"}):d.value>a&&c.push({value:d.value,label:d.label,side:"hi"});return{lo:s,hi:a,offAxis:c,clippedCount:p}}export function limitLabelSide(e,o,n,t,i,r=3){const l=i?-1:1;return(l<0?e-r-o>=n:e+r+o<=t)?l:-l}export function drawOffAxisLimits(e,o,n,t,i,r,l){if(!o.length)return;e.save(),e.fillStyle=i,e.font=`${k(-1)}px system-ui, sans-serif`;const s={lo:0,hi:0},a=k(-1)+2;for(const{value:p,label:c,side:d}of o){const u=s[d]++*a,w=t==="horizontal"?d==="lo"?"\u2190":"\u2192":d==="lo"?"\u2193":"\u2191",v=d==="lo"?`${w} ${c} ${r(p)}`:`${c} ${r(p)} ${w}`;t==="horizontal"?(e.textBaseline="top",e.textAlign=d==="lo"?"left":"right",fillTextOnHalo(e,v,d==="lo"?n.left+2:n.right-2,n.top+2+u,l)):(e.textBaseline=d==="lo"?"bottom":"top",e.textAlign="left",fillTextOnHalo(e,v,n.left+4,d==="lo"?n.bottom-2-u:n.top+2+u,l))}e.restore()}
1
+ import{SHADOW as V,LEADING as G,wireControlHover as B,controlStyle as j,SPACE as w,RADIUS as T,fontPx as F,FONT as L,CLR as f,Z_BASE as oe,menuLayerFor as ie,wireListNavigation as re,MENU_SEARCH_THRESHOLD as U,makeMenuSearchBox as le,markMenuTrigger as N,markNoPrint as P,saveImageBlob as se,openReparentedModal as ae}from"../toolbar.js";import{ICONS as X}from"../icons.js";import{minOf as ce,maxOf as de}from"../../core/utils.js";import{robustFence as K}from"../../stats/math.js";import{fmt as ue,fmtColorbarAxis as pe}from"../../renderer/fmt.js";import{markedTestLabel as fe,DERIVED_LANE_PAD as me}from"../../renderer/testLabel.js";export function openChartExpandModal(e,i,n={}){for(const s of e.querySelectorAll('div[style*="position: absolute"]'))s.style.display==="block"&&s.style.pointerEvents==="none"&&(s.style.display="none");const t=e.querySelector("[data-wmap-chart-expand]"),o=e.getAttribute("style")??"",r=e.dataset.wmapChartGrow??"plot";Object.assign(e.style,{flex:"1",minHeight:"0",alignSelf:"stretch",border:"none",borderRadius:"0"}),e.dataset.wmapExpanded="1";const l=ae([e],{title:i,anchor:n.anchor,ownerDocument:e.ownerDocument,boxSize:he[r],onClosed:()=>{delete e.dataset.wmapExpanded,e.setAttribute("style",o),t&&(t.style.display="flex"),n.onClosed?.()}});if(!l)return delete e.dataset.wmapExpanded,e.setAttribute("style",o),null;if(t&&(t.style.display="none"),r==="rows"){const s=()=>{const a=Number(e.dataset.wmapChartFullH);if(!a)return;const p=l.box.offsetHeight-l.contentWrap.clientHeight;l.setNaturalHeight(`min(90vh, ${Math.ceil(p+a)}px)`)};s(),(e.ownerDocument.defaultView??window).requestAnimationFrame(s)}return l}const he={plot:{width:"min(94vw, 1100px)",height:"min(90vh, 760px)"},rows:{width:"min(94vw, 960px)",height:"min(90vh, 700px)"},square:{width:"min(94vw, 820px)",height:"min(90vh, 860px)"}};export const GRID_ALPHA=.4;export function withAlpha(e,i){const n=e.trim();let t,o,r,l=1;const s=n.match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i),a=n.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i);if(s){const p=s[1].length===3?s[1].split("").map(c=>c+c).join(""):s[1];[t,o,r]=[0,2,4].map(c=>parseInt(p.slice(c,c+2),16)),p.length===8&&(l=parseInt(p.slice(6,8),16)/255)}else if(a)[t,o,r]=[a[1],a[2],a[3]].map(Number),a[4]!==void 0&&(l=a[4].endsWith("%")?parseFloat(a[4])/100:Number(a[4]));else return e;return`rgba(${t}, ${o}, ${r}, ${+(l*i).toFixed(3)})`}const ge={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040",accent:"#1a66cc",limitLine:"#b45309"},Y={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border",accent:"icon-active",limitLine:"warn-text"};export function resolveChartCanvasColors(e){const i=getComputedStyle(e),n={};for(const t of Object.keys(Y)){const o=i.getPropertyValue(`--wmap-${Y[t]}`).trim();n[t]=o||ge[t]}return n.grid=withAlpha(n.border,GRID_ALPHA),n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,i){const n=e.ownerDocument.defaultView??window;let t=0;const o=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,i()}))});return o.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,o.disconnect()}}}export function growCardToFitContent(e,i,n){const t=`${W(e,i)+n}px`;e.style.minHeight!==t&&(e.style.minHeight=t)}function W(e,i){const n=e.ownerDocument.defaultView??window,t=n.getComputedStyle(e);let o=0;for(const r of Array.from(e.children)){if(r===i)continue;const l=r,s=n.getComputedStyle(l);s.display==="none"||s.position==="absolute"||s.position==="fixed"||(o+=l.offsetHeight+(parseFloat(s.marginTop)||0)+(parseFloat(s.marginBottom)||0))}return t.boxSizing==="border-box"&&(o+=(parseFloat(t.paddingTop)||0)+(parseFloat(t.paddingBottom)||0)+(parseFloat(t.borderTopWidth)||0)+(parseFloat(t.borderBottomWidth)||0)),o}function Z(e,i){const n=e.ownerDocument.defaultView??window;let t=0;for(const o of Array.from(e.children)){if(o===i)continue;const r=o,l=n.getComputedStyle(r);l.display==="none"||l.position==="absolute"||l.position==="fixed"||(t+=r.offsetHeight+(parseFloat(l.marginTop)||0)+(parseFloat(l.marginBottom)||0))}return t}export const WAFER_MENU_HINT="<br><em>right-click to chart this wafer</em>";export function populationPhrase(e,i,n){const t=e.toLocaleString(),o=e===1?"die":"dies";return n?e===i?`${t} ${o} ${n}`:`${t} of ${i.toLocaleString()} dies ${n} (partial and edge-excluded dies left out)`:`${t} ${o}`}export function setChartGrow(e,i){e.dataset.wmapChartGrow=i}export function isExpandedCard(e){return e.dataset.wmapExpanded==="1"}export function bodyRoom(e,i,n){const o=(e.ownerDocument.defaultView??window).getComputedStyle(e),r=e.clientHeight-(parseFloat(o.paddingTop)||0)-(parseFloat(o.paddingBottom)||0),l=o.boxSizing==="border-box"?(parseFloat(o.paddingTop)||0)+(parseFloat(o.paddingBottom)||0)+(parseFloat(o.borderTopWidth)||0)+(parseFloat(o.borderBottomWidth)||0):0;return r-(W(e,i)-l)-(n?Z(i,n):0)}export function fitRowsHeight(e,i,n,t,o=0){if(growCardToFitContent(e,i,o+n),e.dataset.wmapChartFullH=String(Math.ceil(W(e,i)+o+t)),!isExpandedCard(e))return n;const r=bodyRoom(e,i)-o;return Math.max(n,Math.min(t,Math.floor(r)))}export function chartFillHeight(e,i,n,t){const o=Z(i,n);return growCardToFitContent(e,i,o+t),Math.max(t,i.clientHeight-o)}export function chartDpr(e){return(e.ownerDocument.defaultView??window).devicePixelRatio||1}export function prepareCanvas(e,i,n,t){const o=chartDpr(e);e.width=Math.max(1,Math.floor(n*o)),e.height=Math.max(1,Math.floor(t*o)),e.style.width=`${n}px`,e.style.height=`${t}px`;const r=e.getContext("2d");return r?(r.setTransform(o,0,0,o,0,0),r.clearRect(0,0,n,t),r.font=`${F(-1)}px system-ui, sans-serif`,r.textBaseline="middle",{ctx:r,theme:resolveChartCanvasColors(i)}):null}export function applyCanvasFlow(e,i=0){let n;if(typeof i=="number")n=i;else{const t=(i.ownerDocument.defaultView??window).getComputedStyle(i);n=i.offsetTop+i.offsetHeight+(parseFloat(t.marginBottom)||0)}e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${n}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:ue(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,i,n){if(!i&&n===void 0)return{tick:formatValue,tickWithUnit:formatValue,unitLabel:""};const{tickFmt:t,axisLabel:o,scaledUnit:r}=pe(e,null,i,"engineering",n);return{tick:t,tickWithUnit:r?l=>`${t(l)} ${r}`:t,unitLabel:o}}const xe=8;export const VERTICAL_TICK_SPACING_PX=28;export function horizontalTickSpacing(e,i,n){return(t,o)=>{const r=makeAxisFormat(i,n,t).tick;return o.reduce((l,s)=>Math.max(l,e.measureText(r(s)).width),0)+xe}}export function drawAxisUnit(e,i,n,t,o){const r={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font=`${F(-1)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="middle",e.fillStyle=o,e.fillText(`(${i})`,n,t),Object.assign(e,r)}export function captionText(e){return e.split(" \xB7 ").map(i=>i.replace(/[\s·]+$/,"").trim()).filter(i=>i&&!/\b(click|clicks|drag|hover|right-click)\b/i.test(i)&&!/\(.*(click|drag).*\)/i.test(i)).join(" \xB7 ")}const M=(e,i)=>{for(let n=e;n&&n!==i.parentElement;n=n.parentElement)if(n.style.display==="none")return!1;return!0},be=/\s*[▾▼]\s*$/,_=new WeakMap;export function describeCard(e){const i=[],n=e.querySelector("[data-wmap-controls]");if(n){for(const l of n.querySelectorAll("button[aria-haspopup]")){if(!M(l,n))continue;const s=(l.textContent??"").replace(be,"").trim();if(!s)continue;const a=l.closest("label"),c=(a?(a.textContent??"").replace(l.textContent??"","").replace(/[:\s]+$/,"").trim():"")||l.getAttribute("aria-label")||"";i.push(c&&c!==s?`${c}: ${s}`:s)}for(const l of n.querySelectorAll('[role="radiogroup"]')){if(!M(l,n))continue;const a=l.querySelector("input:checked")?.closest("label")?.textContent?.trim();a&&i.push(a)}for(const l of n.querySelectorAll('input[type="checkbox"]:checked')){if(!M(l,n))continue;const s=l.closest("label")?.textContent?.trim();s&&i.push(s)}}const t=[];for(const l of e.querySelectorAll("[data-wmap-caption]")){if(!M(l,e))continue;const s=captionText((l.textContent??"").replace(/\s+/g," ").trim());s&&t.push(s)}const o=[];for(const l of e.querySelectorAll("button[aria-pressed]")){const s=l.querySelector("span"),a=s?.style.backgroundColor||s?.style.borderColor,p=(l.textContent??"").trim();p&&a&&a!=="transparent"&&o.push({label:p,color:a})}for(const l of e.querySelectorAll("[data-wmap-legend] > span")){const s=l.firstElementChild,a=s?.style.backgroundColor||s?.style.borderColor,p=(l.textContent??"").trim();p&&a&&a!=="transparent"&&o.push({label:p,color:a})}const r=_.get(e)?.();return r&&(r.controls&&i.push(...r.controls),r.caption!==void 0&&(t.length=0,t.push(...typeof r.caption=="string"?[r.caption]:r.caption)),r.legend&&(o.length=0,o.push(...r.legend))),{controls:i,captions:t,legend:o}}function ye(e){const i=describeCard(e),n=_.get(e)?.();return{controls:i.controls,caption:i.captions,legend:i.legend.length?i.legend:void 0,colorbar:n?.colorbar}}function J(e){for(const i of e.querySelectorAll("[data-wmap-chart-card]")){const n=i.querySelector(":scope > [data-wmap-print-only]");if(!n)continue;const t=describeCard(i);n.replaceChildren();const o=[t.controls.length?t.controls.join(" \xB7 "):"",...t.captions].filter(Boolean);for(const r of o){const l=e.createElement("div");l.textContent=r,n.appendChild(l)}for(const r of i.querySelectorAll("[data-wmap-caption]"))r.setAttribute("data-wmap-noprint","1")}}const Q=new WeakSet;function we(e){if(Q.has(e))return;Q.add(e);const i=e.createElement("style");i.textContent="@media print{[data-wmap-print-only]{display:block!important}}",e.head.appendChild(i);const n=e.defaultView;n?.addEventListener("beforeprint",()=>J(e));try{n?.matchMedia?.("print").addEventListener?.("change",t=>{t.matches&&J(e)})}catch{}}function Ce(e){const i=e.ownerDocument.defaultView;for(let n=e;n&&i;n=n.parentElement){const t=i.getComputedStyle(n).backgroundColor;if(t&&t!=="transparent"&&!/^rgba\(.*,\s*0\)$/.test(t))return t}return"#ffffff"}export function composeChartPng(e,i){const n=e.ownerDocument,t=n.createElement("canvas"),o=Math.max(1,e.width/(e.clientWidth||e.width)),r=e.width,l=n.createElement("canvas").getContext("2d"),s=12*o,a=(g,y="400")=>`${y} ${g*o}px system-ui, -apple-system, "Segoe UI", sans-serif`,p=(g,y)=>l?(l.font=y,l.measureText(g).width):g.length*7*o,c=[];let d=0;if(i){d=s,c.push({kind:"text",text:i.title,x:s,y:d+14*o,font:a(16,"600"),alpha:1}),d+=24*o;const g=i.decor,y=[];g?.controls?.length&&y.push({text:g.controls.join(" \xB7 "),alpha:1});for(const m of g?.caption===void 0?[]:typeof g.caption=="string"?[g.caption]:g.caption)y.push({text:m,alpha:.75});const x=a(12);for(const{text:m,alpha:C}of y){let b="";for(const E of m.split(" ")){const O=b?`${b} ${E}`:E;b&&p(O,x)>r-2*s?(c.push({kind:"text",text:b,x:s,y:d+11*o,font:x,alpha:C}),d+=17*o,b=E):b=O}b&&(c.push({kind:"text",text:b,x:s,y:d+11*o,font:x,alpha:C}),d+=17*o)}if(g?.legend?.length){d+=4*o;const m=a(12);let C=s;for(const b of g.legend){const E=14*o+p(b.label,m)+14*o;C>s&&C+E>r-s&&(C=s,d+=18*o),c.push({kind:"dot",x:C+5*o,y:d+7*o,r:5*o,color:b.color}),c.push({kind:"text",text:b.label,x:C+14*o,y:d+11*o,font:m,alpha:1}),C+=E}d+=20*o}if(g?.colorbar){d+=4*o;const m=a(12),C=g.colorbar;let b=s;c.push({kind:"text",text:C.label,x:b,y:d+10*o,font:m,alpha:1}),b+=p(C.label,m)+10*o,c.push({kind:"text",text:C.lo,x:b,y:d+10*o,font:m,alpha:.75}),b+=p(C.lo,m)+6*o,c.push({kind:"strip",x:b,y:d+1*o,w:200*o,h:10*o,color:C.color}),b+=206*o,c.push({kind:"text",text:C.hi,x:b,y:d+10*o,font:m,alpha:.75}),d+=20*o}d+=6*o}t.width=r,t.height=Math.ceil(e.height+d);const u=t.getContext("2d");if(u.fillStyle=i?Ce(i.card):"#ffffff",u.fillRect(0,0,t.width,t.height),i){const g=n.defaultView,y=g&&g.getComputedStyle(i.card).color||"#222222";u.textBaseline="alphabetic";for(const x of c)if(x.kind==="text")u.font=x.font,u.fillStyle=y,u.globalAlpha=x.alpha,u.fillText(x.text,x.x,x.y),u.globalAlpha=1;else if(x.kind==="dot")u.fillStyle=x.color,u.beginPath(),u.arc(x.x,x.y,x.r,0,Math.PI*2),u.fill();else for(let m=0;m<x.w;m++)u.fillStyle=x.color(m/Math.max(1,x.w-1)),u.fillRect(x.x+m,x.y,1,x.h)}return u.drawImage(e,0,d),t}export function saveCanvasPng(e,i,n,t){composeChartPng(e,t).toBlob(o=>{o&&se(o,i,n)},"image/png")}const ee="10px";export function chartSwatchCss(e,i="solid"){return["display:inline-block",`width:${ee}`,`height:${ee}`,`border-radius:${T.pill}`,`background:${i==="outline"?"transparent":e}`,`border:1px ${i==="outline"?"dashed":"solid"} ${e}`,"box-sizing:border-box","flex-shrink:0"].join(";")}const Se="0.35";export function makeSeriesLegendItem(e,i,n=document){const t=n.createElement("button");t.type="button",Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:w.xs,padding:`${w.xs} ${w.md}`,borderRadius:T.pill,border:`1px solid ${f.menuBorder}`,background:"none",cursor:"pointer",fontSize:L.body,color:f.text,whiteSpace:"nowrap"}),B(t,"bare");const o=n.createElement("span");return o.style.cssText=chartSwatchCss(i),t.append(o,n.createTextNode(e)),{el:t,setState({selected:r,dimmed:l}){t.style.borderColor=r?f.iconActive:f.menuBorder,t.style.fontWeight=r?"600":"400",t.style.opacity=l?Se:"1",t.setAttribute("aria-pressed",r?"true":"false")}}}export function cardFrameStyle(){return{background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:T.container,padding:w.xl,fontVariantNumeric:"tabular-nums"}}export function cardShell(e,i,n=document){const t=n.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",...cardFrameStyle(),minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const o=t.ownerDocument.createElement("div");Object.assign(o.style,{display:"flex",alignItems:"center",gap:w.sm,paddingBottom:"6px",marginBottom:w.md,borderBottom:`1px solid ${f.menuBorder}`});const r=t.ownerDocument.createElement("div");r.textContent=e,Object.assign(r.style,{color:f.value,fontSize:L.heading,fontWeight:"600",flex:"1"}),o.appendChild(r);const l=makeTooltip(t),s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Save as PNG"),s.innerHTML=X.download,Object.assign(s.style,{...j("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:G.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),B(s),attachChartTip(s,t,l,"Save as PNG"),s.addEventListener("click",()=>{const u=t.querySelector("canvas");u&&saveCanvasPng(u,r.textContent||e,i,{card:t,title:r.textContent||e,decor:ye(t)})}),P(s),o.appendChild(s);const a=t.ownerDocument.createElement("button");a.type="button",a.setAttribute("aria-label","Expand"),a.innerHTML=X.expand,Object.assign(a.style,{...j("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:G.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),B(a),attachChartTip(a,t,l,"Expand"),a.dataset.wmapChartExpand="1",a.addEventListener("click",()=>openChartExpandModal(t,r.textContent??e)),P(a),o.appendChild(a),t.appendChild(o);const p=t.ownerDocument.createElement("div");p.dataset.wmapPrintOnly="1",Object.assign(p.style,{display:"none",color:f.label,fontSize:L.body,marginBottom:w.sm}),t.appendChild(p),we(t.ownerDocument);const c=t.ownerDocument.createElement("div");c.dataset.wmapControls="1",Object.assign(c.style,{display:"flex",gap:w.sm,marginBottom:w.md,flexWrap:"wrap",alignItems:"center"}),P(c),t.appendChild(c);const d=t.ownerDocument.createElement("div");return Object.assign(d.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(d),{card:t,heading:r,controlsRow:c,body:d,setPngDecor:u=>{_.set(t,u)}}}export function makeSegmented(e,i,n,t=document,o=!1){const r=t.createElement("div");r.setAttribute("role","radiogroup"),Object.assign(r.style,{display:"inline-flex",border:`1px solid ${f.menuBorder}`,borderRadius:T.control,overflow:"hidden"});const l=`seg-${Math.random().toString(36).slice(2,9)}`,s=[];return e.forEach(([a,p],c)=>{const d=t.createElement("label");Object.assign(d.style,{display:"inline-flex",alignItems:"center",fontSize:L.body,padding:o?"1px 6px":"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:c>0?`1px solid ${f.menuBorder}`:"none"}),B(d,"bare");const u=t.createElement("input");u.type="radio",u.name=l,u.value=a,u.checked=a===i,Object.assign(u.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const g=()=>{d.style.background=u.checked?f.bgActive:f.menuBg,d.style.color=u.checked?f.iconActive:f.text,d.dataset.on=u.checked?"true":"false"};s.push(g),g(),u.addEventListener("change",()=>{if(u.checked){for(const y of s)y();n(a)}}),d.append(u,t.createTextNode(p)),r.appendChild(d)}),r}export function makeBackButton(e,i=document){const n=i.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{...j("outlined"),background:f.menuBg,color:f.label}),B(n),n.addEventListener("click",e),n}export function renderEmptyState(e,i,n){const t=e.ownerDocument.createElement("div");t.textContent=i,Object.assign(t.style,{color:f.label,fontSize:L.body,padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeListSelect(e,i,n,t={}){const{maxWidth:o="200px",ownerDocument:r=document,ariaLabel:l,emptyText:s,searchPlaceholder:a="Filter\u2026",hook:p}=t;let c=i;const d=k=>e.find(H=>H.value===k)?.label??"",u=r.createElement("button");u.type="button",p&&(u.dataset.wmapSelect=p),l&&u.setAttribute("aria-label",l),Object.assign(u.style,{fontSize:L.body,padding:`${w.xxs} ${w.sm}`,background:f.menuBg,color:f.text,border:`1px solid ${f.menuBorder}`,borderRadius:T.control,maxWidth:o,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:w.sm,justifyContent:"space-between"}),B(u),N(u,!1);const g=r.createElement("span");Object.assign(g.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const y=r.createElement("span");y.textContent="\u25BE",y.style.flex="0 0 auto",u.append(g,y);const x=()=>{g.textContent=e.length===0?s??"(none)":d(c)||s||"(none)"};if(x(),e.length===0)return u.disabled=!0,u.style.opacity="0.6",u.style.cursor="default",Object.defineProperty(u,"value",{get:()=>c,set:k=>{c=k,x()}});let m=null;const C=()=>{m&&(m.remove(),m=null,N(u,!1))},b=()=>{C(),u.focus()};function E(){const k=u.getBoundingClientRect(),H=r.defaultView??window,z=Math.max(k.width,220),te=Math.min(k.left,Math.max(4,(H.innerWidth??1/0)-z-4));m=r.createElement("div"),m.setAttribute("role","listbox"),m.setAttribute("aria-label",l??"Options"),Object.assign(m.style,{position:"fixed",top:`${k.bottom+4}px`,left:`${te}px`,background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:T.control,boxShadow:V.menu,zIndex:oe,minWidth:`${z}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const A=[],D=()=>A.filter(h=>h.row.style.display!=="none").map(h=>h.row),I=[],ne=()=>{for(const h of I)h.el.style.display=A.slice(h.first,h.last+1).some(S=>S.row.style.display!=="none")?"":"none"};if(e.length>U){const h=le(S=>{for(const v of A)v.row.style.display=v.label.includes(S)?"":"none";ne()},a,r);Object.assign(h.style,{position:"sticky",top:"0",zIndex:"1",background:f.menuBg}),h.addEventListener("keydown",S=>{if(S.key==="Escape"){b();return}S.key==="ArrowDown"&&(S.preventDefault(),D()[0]?.focus())}),m.appendChild(h)}let q;for(const h of e){if(h.group!==void 0&&h.group!==q){const $=r.createElement("div");$.textContent=h.group,$.setAttribute("role","presentation"),Object.assign($.style,{padding:`${w.sm} ${w.xl} ${w.xxs}`,fontSize:L.sub,color:f.label,fontWeight:"600"}),m.appendChild($),I.push({el:$,first:A.length,last:A.length})}q=h.group;const S=h.value===c,v=r.createElement("div");v.textContent=h.disabled?`${h.label} \u2014 ${h.disabled}`:h.label,v.setAttribute("role","option"),v.setAttribute("aria-selected",S?"true":"false"),h.disabled&&v.setAttribute("aria-disabled","true"),v.tabIndex=-1,Object.assign(v.style,{padding:`${w.sm} ${w.xl}`,fontSize:L.body,cursor:h.disabled?"default":"pointer",color:h.disabled?f.label:S?f.iconActive:f.text,fontWeight:S?"700":"400",opacity:h.disabled?"0.7":"",background:S?f.menuActive:"transparent",whiteSpace:"nowrap"}),v.addEventListener("mouseenter",()=>{!S&&!h.disabled&&(v.style.background=f.menuHover)}),v.addEventListener("mouseleave",()=>{S||(v.style.background="transparent")}),v.addEventListener("click",$=>{$.stopPropagation(),!h.disabled&&(c=h.value,x(),b(),n(h.value))}),m.appendChild(v),A.push({row:v,label:h.label.toLowerCase()}),I.length&&(I[I.length-1].last=A.length-1)}if(re(m,D,b),ie(u).appendChild(m),N(u,!0),e.length<=U){const h=e.findIndex(S=>S.value===c);D()[h>=0?h:0]?.focus()}}u.addEventListener("click",k=>{if(k.stopPropagation(),m){C();return}E()});const O=k=>{if(!u.isConnected){r.removeEventListener("click",O);return}m&&!m.contains(k.target)&&!u.contains(k.target)&&C()};return r.addEventListener("click",O),Object.defineProperty(u,"value",{get:()=>c,set:k=>{c=k,x()}})}export function testOptionLabels(e){const i=e.some(n=>n.derived);return e.map(n=>(i&&!n.derived?me:"")+fe(n,n.testNumber))}export function makeTestSelect(e,i,n,t={}){const{maxWidth:o="200px",emptyText:r="No parametric tests",ownerDocument:l=document}=t;return makeListSelect(testOptionLabels(e).map((s,a)=>({value:String(e[a].testNumber),label:s})),i!==null?String(i):"",s=>n(Number(s)),{maxWidth:o,ownerDocument:l,ariaLabel:"Test",emptyText:r,searchPlaceholder:"Filter tests\u2026"})}export function makeLinkedAxisPrefs(e,i,n,t=document){let o=i?.includeLimits,r=i?.clipOutliers??!1,l=i?.limits??"both";const s=()=>n({includeLimits:o,clipOutliers:r,limits:l});return{get:()=>({includeLimits:o,clipOutliers:r,limits:l}),sync(a,p,c=!1){e.innerHTML="",c&&e.appendChild(makeLimitsSelect(l,d=>{l=d,s()},t)),p&&e.appendChild(makeToggle("Axis includes limits",a,d=>{o=d,s()},t)),e.appendChild(makeToggle("Clip outliers",r,d=>{r=d,s()},t))},set(a){const p=a.limits??"both";return a.includeLimits===o&&a.clipOutliers===r&&p===l?!1:(o=a.includeLimits,r=a.clipOutliers,l=p,!0)}}}export function makeLinkedTestSelect(e,i,n,t={}){let o=i;const r=makeTestSelect(e,o,l=>{l!==o&&(o=l,n(l))},t);return{el:r,get:()=>o,set(l){return o===l||!e.some(s=>s.testNumber===l)?!1:(o=l,r.value=String(l),!0)}}}export const ALL_GROUPS="\0all";export function makeLinkedGroupSelect(e,i,n,t,o={}){let r=n;const l=c=>c??ALL_GROUPS,s=c=>c===ALL_GROUPS?null:c,a=makeLabeledSelect(i,[{value:ALL_GROUPS,label:"All groups"},...e.map(c=>({value:c,label:c}))],l(r),c=>{const d=s(c);d!==r&&(r=d,t(d))},{...o,hook:"group-scope"}),p=a.querySelector('[data-wmap-select="group-scope"]');return{el:a,get:()=>r,set(c){return r===c||c!==null&&!e.includes(c)?!1:(r=c,p&&(p.value=l(c)),!0)}}}export function makeWaferSelect(e,i,n,t={}){const{maxWidth:o="160px",allLabel:r="All wafers",ownerDocument:l=document}=t,s="\0all";return makeListSelect([{value:s,label:r},...e.map((a,p)=>({value:String(p),label:a.label??`#${p}`}))],i===null?s:String(i),a=>n(a===s?null:Number(a)),{maxWidth:o,ownerDocument:l,ariaLabel:"Wafer",searchPlaceholder:"Filter wafers\u2026"})}export function makeToggle(e,i,n,t=document){const o=t.createElement("label");Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:w.xs,fontSize:L.body,color:f.label,cursor:"pointer",userSelect:"none"}),B(o,"bare");const r=t.createElement("input");return r.type="checkbox",r.checked=i,r.style.cssText="margin:0;cursor:pointer;",r.addEventListener("change",()=>n(r.checked)),o.append(r,t.createTextNode(e)),o}export function makeLabeledSelect(e,i,n,t,o={}){const{maxWidth:r="160px",hook:l,ownerDocument:s=document}=o,a=s.createElement("label");a.textContent=e,Object.assign(a.style,{color:f.label,fontSize:L.body,display:"flex",alignItems:"center",gap:w.xs});const p=makeListSelect(i,n,t,{maxWidth:r,ownerDocument:s,hook:l,ariaLabel:e.replace(/:\s*$/,"")});return a.appendChild(p),a}export function makeChartGridWrap(e=document){const i=e.createElement("div");return i.dataset.wmapChartGrid="1",Object.assign(i.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:w.lg,flex:"0 0 auto"}),i}export function makeTooltip(e){const i=e.ownerDocument.createElement("div");return Object.assign(i.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:"rgba(30, 32, 40, 0.93)",color:"#f0f0f2",border:"1px solid rgba(255,255,255,0.10)",borderRadius:T.control,padding:`${w.sm} ${w.xl}`,fontSize:L.sub,fontFamily:L.family,maxWidth:"320px",whiteSpace:"normal",overflowWrap:"break-word",boxShadow:V.menu}),e.appendChild(i),i}export function attachChartTip(e,i,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",o=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,i,o.clientX,o.clientY)}),e.addEventListener("mousemove",o=>{n.style.display==="block"&&positionChartTooltip(n,i,o.clientX,o.clientY)}),e.addEventListener("mouseleave",()=>{n.style.display="none"}),e.addEventListener("click",()=>{n.style.display="none"}),e.addEventListener("focus",()=>{n.textContent=t,n.style.display="block";const o=e.getBoundingClientRect();positionChartTooltip(n,i,o.left+o.width/2,o.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,i,n,t){const o=i.getBoundingClientRect(),r=14,l=4;e.style.left="0",e.style.top="0";const s=e.offsetWidth,a=e.offsetHeight;let p=n-o.left+r,c=t-o.top+r;p+s+l>o.width&&(p=n-o.left-r-s),c+a+l>o.height&&(c=t-o.top-r-a),p=Math.max(l,p),c=Math.max(l,c),e.style.left=`${p}px`,e.style.top=`${c}px`}export const LIMIT_LABEL={test:{lo:"Lo limit",hi:"Hi limit"},spec:{lo:"LSL",hi:"USL"}},LIMIT_DASH={test:[3,3],spec:[10,4]},LIMITS_SHOWN_OPTIONS=[{value:"both",label:"Test + spec"},{value:"test",label:"Test limits"},{value:"spec",label:"Spec limits (LSL/USL)"},{value:"none",label:"None"}];function R(e,i){const[n,t]=i==="test"?[e?.limitLow,e?.limitHigh]:[e?.specLow,e?.specHigh],o=[];return n!==void 0&&Number.isFinite(n)&&o.push({value:n,kind:i,end:"lo",label:LIMIT_LABEL[i].lo}),t!==void 0&&Number.isFinite(t)&&o.push({value:t,kind:i,end:"hi",label:LIMIT_LABEL[i].hi}),o}export function hasBothLimitKinds(e){return R(e,"test").length>0&&R(e,"spec").length>0}export function limitLines(e,i="both"){if(i==="none")return[];const n=R(e,"test"),t=R(e,"spec"),o=i==="test"?n:i==="spec"?t:[...n,...t];return o.length?o:[...n,...t]}export function limitExtent(e){if(!e.length)return{};const i=e.map(n=>n.value);return{lo:ce(i),hi:de(i)}}export function stackLabelRows(e,i=4){const n=e.map((r,l)=>({...r,i:l})).sort((r,l)=>r.start-l.start),t=[],o=new Array(e.length).fill(0);for(const r of n){let l=t.findIndex(s=>r.start>=s+i);l===-1?(l=t.length,t.push(r.end)):t[l]=r.end,o[r.i]=l}return o}export function fillTextOnHalo(e,i,n,t,o){if(o){const r=e.measureText(i),l=2,s=e.fillStyle;e.fillStyle=o,e.fillRect(n-r.actualBoundingBoxLeft-l,t-r.actualBoundingBoxAscent-l,r.actualBoundingBoxLeft+r.actualBoundingBoxRight+2*l,r.actualBoundingBoxAscent+r.actualBoundingBoxDescent+2*l),e.fillStyle=s}e.fillText(i,n,t)}export function strokeLimitLine(e,i,n,t,o,r,l){e.save(),e.strokeStyle=n,e.lineWidth=1,e.setLineDash(LIMIT_DASH[i.kind]),e.beginPath(),e.moveTo(t,o),e.lineTo(r,l),e.stroke(),e.restore()}export function makeLimitsSelect(e,i,n=document){return makeLabeledSelect("Limits:",LIMITS_SHOWN_OPTIONS,e,t=>i(t),{maxWidth:"170px",hook:"limits-shown",ownerDocument:n})}export{K as robustFence};export function shouldIncludeLimitsByDefault(e,i,n,t,o=1/3){if(n===void 0&&t===void 0||!Number.isFinite(e)||!Number.isFinite(i))return!1;const r=Math.min(e,n??e),s=Math.max(i,t??i)-r;if(s<=0)return!0;const a=i-e;return a===0?!0:a/s>=o}export function resolveAxisRange(e){const{dataMin:i,dataMax:n,limits:t=[],includeLimits:o,clipOutliers:r,values:l}=e;let s=i,a=n,p=0;if(r&&l?.length){const d=K(l);if(d){const u=Math.max(s,d.lo),g=Math.min(a,d.hi);g>u&&(p=l.filter(y=>Number.isFinite(y)&&(y<u||y>g)).length,s=u,a=g)}}if(o)for(const d of t)s=Math.min(s,d.value),a=Math.max(a,d.value);s===a&&(s-=1,a+=1);const c=[];for(const d of t)d.value<s?c.push({value:d.value,label:d.label,side:"lo"}):d.value>a&&c.push({value:d.value,label:d.label,side:"hi"});return{lo:s,hi:a,offAxis:c,clippedCount:p}}export function limitLabelSide(e,i,n,t,o,r=3){const l=o?-1:1;return(l<0?e-r-i>=n:e+r+i<=t)?l:-l}export function drawOffAxisLimits(e,i,n,t,o,r,l){if(!i.length)return;e.save(),e.fillStyle=o,e.font=`${F(-1)}px system-ui, sans-serif`;const s={lo:0,hi:0},a=F(-1)+2;for(const{value:p,label:c,side:d}of i){const u=s[d]++*a,g=t==="horizontal"?d==="lo"?"\u2190":"\u2192":d==="lo"?"\u2193":"\u2191",y=d==="lo"?`${g} ${c} ${r(p)}`:`${c} ${r(p)} ${g}`;t==="horizontal"?(e.textBaseline="top",e.textAlign=d==="lo"?"left":"right",fillTextOnHalo(e,y,d==="lo"?n.left+2:n.right-2,n.top+2+u,l)):(e.textBaseline=d==="lo"?"bottom":"top",e.textAlign="left",fillTextOnHalo(e,y,n.left+4,d==="lo"?n.bottom-2-u:n.top+2+u,l))}e.restore()}
@@ -1,2 +1 @@
1
- import{buildCorrelationMatrix as vt,filterCorrelationMatrix as Lt,correlationSampleNote as It}from"../../stats/correlation.js";import{csvField as K,maxOf as $t}from"../../core/utils.js";import{CORRELATION_POSITIVE as ct,CORRELATION_NEGATIVE as dt}from"./palette.js";import{buildFacetTable as St}from"../../stats/facets.js";import{wireControlHover as Tt,controlStyle as Dt,SPACE as v,RADIUS as st,fontPx as ut,FONT as j,CLR as b,saveTextFile as Rt}from"../toolbar.js";import{attachChartTip as At,cardShell as kt,setChartGrow as _t,isExpandedCard as Bt,bodyRoom as Nt,observeResize as Ot,makeTooltip as Wt,positionChartTooltip as Ht,makeWaferSelect as jt,renderEmptyState as Xt,resolveChartCanvasColors as Ft,prepareCanvas as zt,chartDpr as ft}from"./chartShell.js";import{escHtml as q}from"../../core/utils.js";import{unmarkedLabel as mt,derivedCsvCell as pt,DERIVED_CSV_HEADER as ht,DERIVED_MARK as Vt,DERIVED_KEY as Yt}from"../../renderer/testLabel.js";const xt=5,bt=100,gt=20;function lt(g){const d=g.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(d)return[+d[1],+d[2],+d[3]];const p=g.trim().replace("#","");return p.length===6?[parseInt(p.slice(0,2),16),parseInt(p.slice(2,4),16),parseInt(p.slice(4,6),16)]:null}function Pt(g){const d=lt(g);return d?(d[0]*299+d[1]*587+d[2]*114)/1e3>140?"#111":"#fff":"#000"}function Gt(g,d,p){const N=lt(g);if(!N)return g;const J=Math.round(d[0]+(N[0]-d[0])*p),O=Math.round(d[1]+(N[1]-d[1])*p),l=Math.round(d[2]+(N[2]-d[2])*p);return`rgb(${J},${O},${l})`}export function renderCorrelationPanel(g){const{title:d="Test correlation matrix",items:p,testDefs:N,onSaveImage:J,onSelectPair:O}=g,{card:l,body:L,controlsRow:Q}=kt(d,J,g.ownerDocument);_t(l,"square"),L.style.overflowX="auto",l.style.alignSelf="start";let V=gt,X=null;const F=l.ownerDocument.createElement("label");F.textContent="Max tests:",Object.assign(F.style,{color:b.label,fontSize:j.body,display:"flex",alignItems:"center",gap:v.xs});const w=l.ownerDocument.createElement("input");w.type="number",w.min=String(xt),w.max=String(bt),w.value=String(V),Object.assign(w.style,{width:"52px",fontSize:j.body,padding:`${v.xxs} ${v.xs}`,background:b.menuBg,color:b.value,border:`1px solid ${b.menuBorder}`,borderRadius:st.control}),w.addEventListener("change",()=>{const e=Math.max(xt,Math.min(bt,parseInt(w.value,10)||gt));w.value=String(e),e!==V&&(V=e,tt())}),F.appendChild(w),Q.appendChild(F),p.length>1&&Q.appendChild(jt(p,X,e=>{X=e,tt()},{ownerDocument:l.ownerDocument}));function yt(){return X!==null?p[X]?[p[X]]:[]:p}if(g.onSaveText){const e=l.ownerDocument.createElement("button");e.type="button",e.textContent="Correlation CSV",Object.assign(e.style,{...Dt("outlined"),color:b.text}),Tt(e),e.addEventListener("click",()=>{const o=rt;if(!o)return;const a=o.tests.some(f=>f.derived),u=["Test X,Test X number,Test Y,Test Y number,r,n"+(a?`,Test X ${ht.toLowerCase()},Test Y ${ht.toLowerCase()}`:"")];for(let f=0;f<o.tests.length;f++)for(let i=f+1;i<o.tests.length;i++){const y=o.cells.find(h=>h.xIndex===i&&h.yIndex===f);y&&u.push([K(mt(o.tests[i].label)),String(o.tests[i].testNumber),K(mt(o.tests[f].label)),String(o.tests[f].testNumber),y.r===null?"":y.r.toFixed(6),String(y.n),...a?[K(pt(o.tests[i])),K(pt(o.tests[f]))]:[]].join(","))}Rt(u.join(`
2
- `),"test-correlation.csv","text/csv",g.onSaveText)}),Q.appendChild(e)}const k=l.ownerDocument.createElement("div");Object.assign(k.style,{display:"flex",flexDirection:"column",gap:v.xs,marginBottom:v.sm}),l.insertBefore(k,L);const I=Wt(l);At(F,l,I,"Cap on how many tests the matrix includes (strongest correlations kept first)");let Z=()=>{};function Et(e,o,a,u,f,i,y,h){k.innerHTML="";const _=It(y);if(_){const x=l.ownerDocument.createElement("div");x.textContent=`${_} \u2014 r is an estimate, not the whole population.`,Object.assign(x.style,{color:b.label,fontSize:j.body}),k.appendChild(x)}if(f.length>0){const x=l.ownerDocument.createElement("div");x.textContent=`\u26A0 Mixed ${f.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(x.style,{color:b.warnText,background:b.warnBg,border:`1px solid ${b.warnBorder}`,borderRadius:st.control,padding:`${v.xs} ${v.md}`,fontSize:j.body}),k.appendChild(x)}const M=l.ownerDocument.createElement("span");Object.assign(M.style,{display:"inline-flex",alignItems:"center",gap:v.sm,color:b.label,fontSize:j.body,flexWrap:"wrap"});const W=l.ownerDocument.createElement("span");W.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")+(h?` ${Vt} ${Yt} \xB7`:""),M.appendChild(W);const r=l.ownerDocument.createElement("span");Object.assign(r.style,{display:"inline-flex",alignItems:"center",gap:v.xs});const $=l.ownerDocument.createElement("span");$.textContent="\u22121";const Y=l.ownerDocument.createElement("span");Object.assign(Y.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:st.control,border:`1px solid ${b.menuBorder}`,background:`linear-gradient(to right, ${dt}, ${b.menuBg}, ${ct})`});const z=l.ownerDocument.createElement("span");z.textContent="+1",r.append($,Y,z),M.appendChild(r),k.appendChild(M);const S=l.ownerDocument.createElement("span");if(Object.assign(S.style,{color:b.value,fontSize:j.body,fontWeight:"500"}),e===0&&o===0)S.textContent=u?`No significant correlations found \u2014 strongest pair: ${u.xLabel} \u2194 ${u.yLabel} (r = ${u.r.toFixed(2)})`:"No significant correlations found";else{const x=[];e>0&&x.push(`${e} strong (|r| \u2265 0.7)`),o>0&&x.push(`${o} moderate (0.4\u20130.7)`);const P=e+o;let s=x.join(", ")+` pair${P!==1?"s":""} found`;a>0&&(s+=` \xB7 ${a} weak pair${a!==1?"s":""} not shown`),S.textContent=s}k.appendChild(S)}function Mt(e){if(L.innerHTML="",e.tests.length<2)return Xt(L,"Need at least two parametric tests for a correlation matrix."),()=>{};const o=l.ownerDocument.createElement("canvas");o.style.display="block",o.style.cursor="default",L.appendChild(o);const a=e.tests.length,u=s=>s.label.split(" (#")[0],f=Math.min(14,$t(e.tests.map(s=>u(s).length))),i=f*6.5+8,y=10,h=Math.round(y*6.5*Math.sin(Math.PI/4))+14,_=14,M=28,W=44;let r=-1,$=-1;const Y=96;function z(s){const c=Math.max(0,s-i),m=Math.floor(c/a);if(!Bt(l))return Math.max(_,Math.min(W,m));const n=Math.floor((Nt(l,L,o)-h-4)/a);return Math.max(_,Math.min(Y,m,n))}const S=new Map;for(const s of e.cells){let c=S.get(s.yIndex);c||(c=[],S.set(s.yIndex,c)),c.push(s)}function x(){const s=Ft(l),c=lt(s.bg)??[255,255,255],m=L.clientWidth,n=z(m),H=n*a,T=h+n*a+4,D=i+H,R=zt(o,l,D,T);if(!R)return;const{ctx:t}=R;t.font=`${ut(-1)}px system-ui, sans-serif`,t.fillStyle=s.text,e.tests.forEach((A,E)=>{const C=u(A),B=C.length>y?`${C.slice(0,y-1)}\u2026`:C,et=i+E*n+n/2;t.save(),t.translate(et,h-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=E===r?s.text:s.textMuted,t.fillText(B,0,0),t.restore()}),e.tests.forEach((A,E)=>{const C=u(A),B=h+E*n,et=B+n/2;t.font=`${ut(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle",t.fillStyle=E===$?s.text:s.textMuted,t.fillText(C.length>f?`${C.slice(0,f-1)}\u2026`:C,i-4,et),(S.get(E)??[]).forEach(it=>{const nt=it.xIndex,G=i+nt*n,wt=nt===r&&E===$,at=nt===E,U=it.r;if(U===null){t.fillStyle=s.border,t.globalAlpha=.4,t.fillRect(G+1,B+1,n-2,n-2),t.globalAlpha=1;return}let ot=s.bgHover;at?t.fillStyle=s.bgHover:(ot=Gt(U>=0?ct:dt,c,Math.abs(U)),t.fillStyle=ot),t.fillRect(G+1,B+1,n-2,n-2),n>=M&&!at&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Pt(ot),t.textAlign="center",t.textBaseline="middle",t.fillText(U.toFixed(2),G+n/2,B+n/2)),wt&&(t.strokeStyle=s.text,t.lineWidth=2,t.strokeRect(G+1,B+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=s.bg,t.lineWidth=1;for(let A=0;A<=a;A++){const E=i+A*n;t.beginPath(),t.moveTo(E,h),t.lineTo(E,h+a*n),t.stroke();const C=h+A*n;t.beginPath(),t.moveTo(i,C),t.lineTo(i+a*n,C),t.stroke()}}function P(s){const c=o.getBoundingClientRect(),m=L.clientWidth,n=z(m),H=(s.clientX-c.left)*(o.width/ft(o)/c.width),T=(s.clientY-c.top)*(o.height/ft(o)/c.height),D=Math.floor((H-i)/n),R=Math.floor((T-h)/n);return D>=0&&D<a&&R>=0&&R<a?{xi:D,yi:R}:null}return o.addEventListener("mousemove",s=>{const c=P(s);if(!c){I.style.display="none",o.style.cursor="default";return}const{xi:m,yi:n}=c,H=m===n;o.style.cursor=H||!O?"default":"pointer";const T=e.cells.find(t=>t.xIndex===m&&t.yIndex===n),D=e.tests[m].label,R=e.tests[n].label;H?I.innerHTML=`<strong>${q(D)}</strong>`:T?.r!==null&&T?.r!==void 0?I.innerHTML=`<strong>${q(u(e.tests[n]))}</strong> (#${e.tests[n].testNumber}) vs <strong>${q(u(e.tests[m]))}</strong> (#${e.tests[m].testNumber})<br>r = ${T.r.toFixed(4)} \xB7 n = ${T.n.toLocaleString()}${O?"<br><em>click to view in scatter</em>":""}`:I.innerHTML=`${q(`${R} vs ${D}`)}<br><em>insufficient data</em>`,I.style.display="block",Ht(I,l,s.clientX,s.clientY)}),o.addEventListener("mouseleave",()=>{I.style.display="none"}),o.addEventListener("click",s=>{const c=P(s);if(!c||!O)return;const{xi:m,yi:n}=c;m!==n&&(r=m,$=n,x(),O(e.tests[m].testNumber,e.tests[n].testNumber))}),x}let rt=null;function tt(){const e=yt(),o=e.flatMap(r=>r.dies??[]),a=vt(o,N),{matrix:u,strongPairs:f,moderatePairs:i,hiddenWeakPairs:y,strongestPair:h}=Lt(a,{minTests:6,maxTests:V}),_=St(e,{facetableOnly:!0}).filter(r=>r.splittable).map(r=>r.label),M=u.cells.filter(r=>r.xIndex!==r.yIndex).map(r=>r.n).sort((r,$)=>r-$),W=M.length?M[Math.floor(M.length/2)]:null;rt=u,Et(f,i,y,h,_,W,a.sample,u.tests.some(r=>r.derived)),Z=Mt(u),Z()}const Ct=Ot(l,()=>Z());return tt(),{card:l,destroy:()=>Ct.disconnect()}}
1
+ import{buildCorrelationMatrix as vt,filterCorrelationMatrix as It,correlationSampleNote as $t}from"../../stats/correlation.js";import{maxOf as Tt}from"../../core/utils.js";import{CORRELATION_POSITIVE as st,CORRELATION_NEGATIVE as lt}from"./palette.js";import{buildFacetTable as St}from"../../stats/facets.js";import{wireControlHover as Dt,controlStyle as Rt,SPACE as v,RADIUS as rt,fontPx as dt,FONT as X,CLR as g}from"../toolbar.js";import{exportCsv as At}from"../tableExport.js";import{attachChartTip as kt,cardShell as _t,setChartGrow as Bt,isExpandedCard as Nt,bodyRoom as Ot,observeResize as Wt,makeTooltip as Ht,positionChartTooltip as Xt,makeWaferSelect as jt,renderEmptyState as zt,resolveChartCanvasColors as Ft,prepareCanvas as Vt,chartDpr as ut}from"./chartShell.js";import{escHtml as K}from"../../core/utils.js";import{unmarkedLabel as mt,derivedCsvCell as pt,DERIVED_CSV_HEADER as ft,DERIVED_MARK as Pt,DERIVED_KEY as Yt}from"../../renderer/testLabel.js";const ht=5,xt=100,gt=20;function q(b){const d=b.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(d)return[+d[1],+d[2],+d[3]];const h=b.trim().replace("#","");return h.length===6?[parseInt(h.slice(0,2),16),parseInt(h.slice(2,4),16),parseInt(h.slice(4,6),16)]:null}function Gt(b){const d=q(b);return d?(d[0]*299+d[1]*587+d[2]*114)/1e3>140?"#111":"#fff":"#000"}function bt(b,d,h){const O=q(b);if(!O)return b;const J=Math.round(d[0]+(O[0]-d[0])*h),W=Math.round(d[1]+(O[1]-d[1])*h),r=Math.round(d[2]+(O[2]-d[2])*h);return`rgb(${J},${W},${r})`}export function renderCorrelationPanel(b){const{title:d="Test correlation matrix",items:h,testDefs:O,onSaveImage:J,onSelectPair:W}=b,{card:r,body:I,controlsRow:Q,setPngDecor:yt}=_t(d,J,b.ownerDocument);yt(()=>{const e=q(r.ownerDocument.defaultView?.getComputedStyle(r).backgroundColor??"")??[255,255,255];return{colorbar:{label:"Pearson r",lo:"\u22121",hi:"+1",color:o=>bt(o<.5?lt:st,e,Math.abs(2*o-1))}}}),Bt(r,"square"),I.style.overflowX="auto",r.style.alignSelf="start";let V=gt,j=null;const z=r.ownerDocument.createElement("label");z.textContent="Max tests:",Object.assign(z.style,{color:g.label,fontSize:X.body,display:"flex",alignItems:"center",gap:v.xs});const E=r.ownerDocument.createElement("input");E.type="number",E.min=String(ht),E.max=String(xt),E.value=String(V),Object.assign(E.style,{width:"52px",fontSize:X.body,padding:`${v.xxs} ${v.xs}`,background:g.menuBg,color:g.value,border:`1px solid ${g.menuBorder}`,borderRadius:rt.control}),E.addEventListener("change",()=>{const e=Math.max(ht,Math.min(xt,parseInt(E.value,10)||gt));E.value=String(e),e!==V&&(V=e,tt())}),z.appendChild(E),Q.appendChild(z),h.length>1&&Q.appendChild(jt(h,j,e=>{j=e,tt()},{ownerDocument:r.ownerDocument}));function Ct(){return j!==null?h[j]?[h[j]]:[]:h}if(b.onSaveText){const e=r.ownerDocument.createElement("button");e.type="button",e.textContent="Correlation CSV",Object.assign(e.style,{...Rt("outlined"),color:g.text}),Dt(e),e.addEventListener("click",()=>{const o=it;if(!o)return;const u=o.tests.some(s=>s.derived),m=[];for(let s=0;s<o.tests.length;s++)for(let a=s+1;a<o.tests.length;a++){const y=o.cells.find(x=>x.xIndex===a&&x.yIndex===s);y&&m.push({x:o.tests[a],y:o.tests[s],r:y.r,n:y.n})}At([{header:"Test X",get:s=>mt(s.x.label)},{header:"Test X number",get:s=>s.x.testNumber},{header:"Test Y",get:s=>mt(s.y.label)},{header:"Test Y number",get:s=>s.y.testNumber},{header:"r",get:s=>s.r===null?"":s.r.toFixed(6)},{header:"n",get:s=>s.n},...u?[{header:`Test X ${ft.toLowerCase()}`,get:s=>pt(s.x)},{header:`Test Y ${ft.toLowerCase()}`,get:s=>pt(s.y)}]:[]],m,"test-correlation.csv",b.onSaveText)}),Q.appendChild(e)}const k=r.ownerDocument.createElement("div");Object.assign(k.style,{display:"flex",flexDirection:"column",gap:v.xs,marginBottom:v.sm}),r.insertBefore(k,I);const $=Ht(r);kt(z,r,$,"Cap on how many tests the matrix includes (strongest correlations kept first)");let Z=()=>{};function wt(e,o,u,m,s,a,y,x){k.innerHTML="";const _=$t(y);if(_){const p=r.ownerDocument.createElement("div");p.dataset.wmapCaption="1",p.textContent=`${_} \u2014 r is an estimate, not the whole population.`,Object.assign(p.style,{color:g.label,fontSize:X.body}),k.appendChild(p)}if(s.length>0){const p=r.ownerDocument.createElement("div");p.dataset.wmapCaption="1",p.textContent=`\u26A0 Mixed ${s.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(p.style,{color:g.warnText,background:g.warnBg,border:`1px solid ${g.warnBorder}`,borderRadius:rt.control,padding:`${v.xs} ${v.md}`,fontSize:X.body}),k.appendChild(p)}const w=r.ownerDocument.createElement("span");Object.assign(w.style,{display:"inline-flex",alignItems:"center",gap:v.sm,color:g.label,fontSize:X.body,flexWrap:"wrap"});const B=r.ownerDocument.createElement("span");B.dataset.wmapCaption="1",B.textContent=(a!==null?`Pearson r \xB7 n \u2248 ${a.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")+(x?` ${Pt} ${Yt} \xB7`:""),w.appendChild(B);const i=r.ownerDocument.createElement("span");Object.assign(i.style,{display:"inline-flex",alignItems:"center",gap:v.xs});const T=r.ownerDocument.createElement("span");T.textContent="\u22121";const P=r.ownerDocument.createElement("span");Object.assign(P.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:rt.control,border:`1px solid ${g.menuBorder}`,background:`linear-gradient(to right, ${lt}, ${g.menuBg}, ${st})`});const F=r.ownerDocument.createElement("span");F.textContent="+1",i.append(T,P,F),w.appendChild(i),k.appendChild(w);const L=r.ownerDocument.createElement("span");if(L.dataset.wmapCaption="1",Object.assign(L.style,{color:g.value,fontSize:X.body,fontWeight:"500"}),e===0&&o===0)L.textContent=m?`No significant correlations found \u2014 strongest pair: ${m.xLabel} \u2194 ${m.yLabel} (r = ${m.r.toFixed(2)})`:"No significant correlations found";else{const p=[];e>0&&p.push(`${e} strong (|r| \u2265 0.7)`),o>0&&p.push(`${o} moderate (0.4\u20130.7)`);const Y=e+o;let l=p.join(", ")+` pair${Y!==1?"s":""} found`;u>0&&(l+=` \xB7 ${u} weak pair${u!==1?"s":""} not shown`),L.textContent=l}k.appendChild(L)}function Mt(e){if(I.innerHTML="",e.tests.length<2)return zt(I,"Need at least two parametric tests for a correlation matrix."),()=>{};const o=r.ownerDocument.createElement("canvas");o.style.display="block",o.style.cursor="default",I.appendChild(o);const u=e.tests.length,m=l=>l.label.split(" (#")[0],s=Math.min(14,Tt(e.tests.map(l=>m(l).length))),a=s*6.5+8,y=10,x=Math.round(y*6.5*Math.sin(Math.PI/4))+14,_=14,w=28,B=44;let i=-1,T=-1;const P=96;function F(l){const c=Math.max(0,l-a),f=Math.floor(c/u);if(!Nt(r))return Math.max(_,Math.min(B,f));const n=Math.floor((Ot(r,I,o)-x-4)/u);return Math.max(_,Math.min(P,f,n))}const L=new Map;for(const l of e.cells){let c=L.get(l.yIndex);c||(c=[],L.set(l.yIndex,c)),c.push(l)}function p(){const l=Ft(r),c=q(l.bg)??[255,255,255],f=I.clientWidth,n=F(f),H=n*u,S=x+n*u+4,D=a+H,R=Vt(o,r,D,S);if(!R)return;const{ctx:t}=R;t.font=`${dt(-1)}px system-ui, sans-serif`,t.fillStyle=l.text,e.tests.forEach((A,C)=>{const M=m(A),N=M.length>y?`${M.slice(0,y-1)}\u2026`:M,et=a+C*n+n/2;t.save(),t.translate(et,x-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=C===i?l.text:l.textMuted,t.fillText(N,0,0),t.restore()}),e.tests.forEach((A,C)=>{const M=m(A),N=x+C*n,et=N+n/2;t.font=`${dt(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle",t.fillStyle=C===T?l.text:l.textMuted,t.fillText(M.length>s?`${M.slice(0,s-1)}\u2026`:M,a-4,et),(L.get(C)??[]).forEach(at=>{const nt=at.xIndex,G=a+nt*n,Lt=nt===i&&C===T,ct=nt===C,U=at.r;if(U===null){t.fillStyle=l.border,t.globalAlpha=.4,t.fillRect(G+1,N+1,n-2,n-2),t.globalAlpha=1;return}let ot=l.bgHover;ct?t.fillStyle=l.bgHover:(ot=bt(U>=0?st:lt,c,Math.abs(U)),t.fillStyle=ot),t.fillRect(G+1,N+1,n-2,n-2),n>=w&&!ct&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Gt(ot),t.textAlign="center",t.textBaseline="middle",t.fillText(U.toFixed(2),G+n/2,N+n/2)),Lt&&(t.strokeStyle=l.text,t.lineWidth=2,t.strokeRect(G+1,N+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=l.bg,t.lineWidth=1;for(let A=0;A<=u;A++){const C=a+A*n;t.beginPath(),t.moveTo(C,x),t.lineTo(C,x+u*n),t.stroke();const M=x+A*n;t.beginPath(),t.moveTo(a,M),t.lineTo(a+u*n,M),t.stroke()}}function Y(l){const c=o.getBoundingClientRect(),f=I.clientWidth,n=F(f),H=(l.clientX-c.left)*(o.width/ut(o)/c.width),S=(l.clientY-c.top)*(o.height/ut(o)/c.height),D=Math.floor((H-a)/n),R=Math.floor((S-x)/n);return D>=0&&D<u&&R>=0&&R<u?{xi:D,yi:R}:null}return o.addEventListener("mousemove",l=>{const c=Y(l);if(!c){$.style.display="none",o.style.cursor="default";return}const{xi:f,yi:n}=c,H=f===n;o.style.cursor=H||!W?"default":"pointer";const S=e.cells.find(t=>t.xIndex===f&&t.yIndex===n),D=e.tests[f].label,R=e.tests[n].label;H?$.innerHTML=`<strong>${K(D)}</strong>`:S?.r!==null&&S?.r!==void 0?$.innerHTML=`<strong>${K(m(e.tests[n]))}</strong> (#${e.tests[n].testNumber}) vs <strong>${K(m(e.tests[f]))}</strong> (#${e.tests[f].testNumber})<br>r = ${S.r.toFixed(4)} \xB7 n = ${S.n.toLocaleString()}${W?"<br><em>click to view in scatter</em>":""}`:$.innerHTML=`${K(`${R} vs ${D}`)}<br><em>insufficient data</em>`,$.style.display="block",Xt($,r,l.clientX,l.clientY)}),o.addEventListener("mouseleave",()=>{$.style.display="none"}),o.addEventListener("click",l=>{const c=Y(l);if(!c||!W)return;const{xi:f,yi:n}=c;f!==n&&(i=f,T=n,p(),W(e.tests[f].testNumber,e.tests[n].testNumber))}),p}let it=null;function tt(){const e=Ct(),o=e.flatMap(i=>i.dies??[]),u=vt(o,O),{matrix:m,strongPairs:s,moderatePairs:a,hiddenWeakPairs:y,strongestPair:x}=It(u,{minTests:6,maxTests:V}),_=St(e,{facetableOnly:!0}).filter(i=>i.splittable).map(i=>i.label),w=m.cells.filter(i=>i.xIndex!==i.yIndex).map(i=>i.n).sort((i,T)=>i-T),B=w.length?w[Math.floor(w.length/2)]:null;it=m,wt(s,a,y,x,_,B,u.sample,m.tests.some(i=>i.derived)),Z=Mt(m),Z()}const Et=Wt(r,()=>Z());return tt(),{card:r,destroy:()=>Et.disconnect()}}
@@ -35,6 +35,8 @@ export interface GroupedBarPlotOptions {
35
35
  /** Tooltip HTML for a hovered sub-bar. */
36
36
  /** HTML for the hover tooltip. Escape every label with `escHtml` (core/utils.ts): they come from input files. */
37
37
  tooltipHtml: (rowIndex: number, groupIndex: number) => string;
38
+ /** Click a sub-bar: called with its row, its group and the click, so the host can open a menu there. */
39
+ onRowClick?: (rowIndex: number, groupIndex: number, e: MouseEvent) => void;
38
40
  }
39
41
  export interface GroupedBarPlotHandle {
40
42
  /** Redraw — call when the theme changes or data is replaced. */
@@ -1 +1 @@
1
- import{categorical as Y}from"./palette.js";import{SPACE as E,FONT as q,CLR as J}from"../toolbar.js";import{observeResize as K,positionChartTooltip as Q,fitRowsHeight as Z,setChartGrow as tt,prepareCanvas as et,chartSwatchCss as nt,PADDING as c,VALUE_WIDTH as D}from"./chartShell.js";const lt=8,w=1,a=14,ot=8;export function renderGroupedBarPlot(h,C,p,U){const{rows:v,groups:L,labelWidth:T,tooltipHtml:$,maxVisibleRows:j=ot,showLegend:k=!0,maxLabelChars:H,emptyBarText:R,colorOf:B=Y}=U,f=h.ownerDocument;tt(h,"rows");const A=Math.max(1,L.length),M=A*a+(A-1)*w,u=M+lt;let W=0;if(k){const e=f.createElement("div");Object.assign(e.style,{display:"flex",flexWrap:"wrap",gap:`${E.xs} ${E.xl}`,marginBottom:E.xs}),L.forEach((l,n)=>{const t=f.createElement("span");Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:E.sm,fontSize:q.body,color:J.text});const o=f.createElement("span");o.style.cssText=nt(B(n));const i=f.createElement("span");i.textContent=l,t.append(o,i),e.appendChild(t)}),C.appendChild(e),W=e.offsetHeight}const d=f.createElement("div"),G=c*2+Math.min(v.length,j)*u;Object.assign(d.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${G}px`,scrollbarGutter:"stable"}),C.appendChild(d);const s=f.createElement("canvas");s.style.display="block",s.style.cursor="default",d.appendChild(s);let r=null;function _(){const e=c+T,l=Math.max(10,s.clientWidth-e-D-c);return{barX:e,barMaxWidth:l}}function z(e,l){const n=Math.floor((l-c)/u);if(n<0||n>=v.length)return null;const t=l-c-n*u,o=Math.floor(t/(a+w));if(o<0||o>=A)return null;const{barX:i,barMaxWidth:m}=_();return e<i||e>i+m?null:{row:n,group:o}}function g(){const e=c*2+v.length*u;d.style.maxHeight=`${Z(h,C,G,e,W)}px`;const l=d.clientWidth,n=et(s,h,l,e);if(!n)return;const{ctx:t,theme:o}=n,{barX:i,barMaxWidth:m}=_();v.forEach((x,P)=>{const b=c+P*u;if(r&&r.row===P){const y=b+r.group*(a+w);t.fillStyle=o.bgHover,t.fillRect(0,y-1,l,a+2)}t.fillStyle=o.text,t.textAlign="right";const F=H!==void 0&&x.label.length>H?`${x.label.slice(0,H-1)}\u2026`:x.label;t.fillText(F,c+T-8,b+M/2),x.bars.forEach((y,X)=>{const S=b+X*(a+w);if(t.fillStyle=o.track,t.fillRect(i,S,m,a),y.fraction===null){R&&(t.fillStyle=o.textMuted,t.textAlign="left",t.fillText(R,i+4,S+a/2));return}const N=Math.max(y.fraction>0?1:0,y.fraction*m);t.fillStyle=B(X),t.fillRect(i,S,N,a)}),t.fillStyle=o.text,t.textAlign="right",t.fillText(x.trailing,i+m+D,b+M/2)})}const I=e=>{const l=s.getBoundingClientRect(),n=z(e.clientX-l.left,e.clientY-l.top),t=n?.row!==r?.row||n?.group!==r?.group;if(r=n,t&&g(),!n){p.style.display="none";return}p.innerHTML=$(n.row,n.group),p.style.display="block",Q(p,h,e.clientX,e.clientY)},O=()=>{r&&(r=null,g()),p.style.display="none"};s.addEventListener("mousemove",I),s.addEventListener("mouseleave",O);const V=K(h,()=>g());return g(),{draw:g,destroy:()=>{V?.disconnect(),s.removeEventListener("mousemove",I),s.removeEventListener("mouseleave",O)}}}
1
+ import{categorical as q}from"./palette.js";import{SPACE as E,FONT as J,CLR as K}from"../toolbar.js";import{observeResize as Q,positionChartTooltip as Z,fitRowsHeight as tt,setChartGrow as et,prepareCanvas as nt,chartSwatchCss as lt,PADDING as c,VALUE_WIDTH as $}from"./chartShell.js";const ot=8,C=1,a=14,it=8;export function renderGroupedBarPlot(f,L,h,v){const{rows:w,groups:S,labelWidth:T,tooltipHtml:j,maxVisibleRows:z=it,showLegend:V=!0,maxLabelChars:R,emptyBarText:B,colorOf:k=q}=v,d=f.ownerDocument;et(f,"rows");const H=Math.max(1,S.length),A=H*a+(H-1)*C,u=A+ot;let W=0;if(V){const t=d.createElement("div");t.dataset.wmapLegend="1",Object.assign(t.style,{display:"flex",flexWrap:"wrap",gap:`${E.xs} ${E.xl}`,marginBottom:E.xs}),S.forEach((l,n)=>{const e=d.createElement("span");Object.assign(e.style,{display:"inline-flex",alignItems:"center",gap:E.sm,fontSize:J.body,color:K.text});const i=d.createElement("span");i.style.cssText=lt(k(n));const s=d.createElement("span");s.textContent=l,e.append(i,s),t.appendChild(e)}),L.appendChild(t),W=t.offsetHeight}const g=d.createElement("div"),G=c*2+Math.min(w.length,z)*u;Object.assign(g.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${G}px`,scrollbarGutter:"stable"}),L.appendChild(g);const o=d.createElement("canvas");o.style.display="block",o.style.cursor="default",g.appendChild(o);let r=null;function X(){const t=c+T,l=Math.max(10,o.clientWidth-t-$-c);return{barX:t,barMaxWidth:l}}function _(t,l){const n=Math.floor((l-c)/u);if(n<0||n>=w.length)return null;const e=l-c-n*u,i=Math.floor(e/(a+C));if(i<0||i>=H)return null;const{barX:s,barMaxWidth:m}=X();return t<s||t>s+m?null:{row:n,group:i}}function p(){const t=c*2+w.length*u;g.style.maxHeight=`${tt(f,L,G,t,W)}px`;const l=g.clientWidth,n=nt(o,f,l,t);if(!n)return;const{ctx:e,theme:i}=n,{barX:s,barMaxWidth:m}=X();w.forEach((x,D)=>{const b=c+D*u;if(r&&r.row===D){const y=b+r.group*(a+C);e.fillStyle=i.bgHover,e.fillRect(0,y-1,l,a+2)}e.fillStyle=i.text,e.textAlign="right";const F=R!==void 0&&x.label.length>R?`${x.label.slice(0,R-1)}\u2026`:x.label;e.fillText(F,c+T-8,b+A/2),x.bars.forEach((y,U)=>{const M=b+U*(a+C);if(e.fillStyle=i.track,e.fillRect(s,M,m,a),y.fraction===null){B&&(e.fillStyle=i.textMuted,e.textAlign="left",e.fillText(B,s+4,M+a/2));return}const N=Math.max(y.fraction>0?1:0,y.fraction*m);e.fillStyle=k(U),e.fillRect(s,M,N,a)}),e.fillStyle=i.text,e.textAlign="right",e.fillText(x.trailing,s+m+$,b+A/2)})}const I=t=>{const l=o.getBoundingClientRect(),n=_(t.clientX-l.left,t.clientY-l.top),e=n?.row!==r?.row||n?.group!==r?.group;if(r=n,e&&p(),!n){h.style.display="none";return}h.innerHTML=j(n.row,n.group),h.style.display="block",Z(h,f,t.clientX,t.clientY)},O=()=>{r&&(r=null,p()),h.style.display="none"},P=t=>{const l=o.getBoundingClientRect(),n=_(t.clientX-l.left,t.clientY-l.top);n&&v.onRowClick&&(h.style.display="none",v.onRowClick(n.row,n.group,t))};o.addEventListener("mousemove",I),o.addEventListener("mouseleave",O),v.onRowClick&&(o.style.cursor="pointer",o.addEventListener("click",P));const Y=Q(f,()=>p());return p(),{draw:p,destroy:()=>{Y?.disconnect(),o.removeEventListener("mousemove",I),o.removeEventListener("mouseleave",O),o.removeEventListener("click",P)}}}
@@ -1,5 +1,6 @@
1
1
  import { type HistogramItem } from '../../stats/histogram.js';
2
2
  import type { TestDef } from '../../renderer/buildWaferMap.js';
3
+ import type { Die } from '../../core/dies.js';
3
4
  import { type SaveImageHandler, type AxisPrefs } from './chartShell.js';
4
5
  export interface HistogramPanelOptions {
5
6
  /** Initial axis toggles, shared with the sibling distribution panels. */
@@ -27,6 +28,21 @@ export interface HistogramPanelOptions {
27
28
  /** Fired when the user narrows to one group by clicking it in the legend.
28
29
  * The Insights tab owns the scope; this panel only reports the gesture. */
29
30
  onGroupChange?: (key: string | null) => void;
31
+ /**
32
+ * Click a bar: the dies it counts, per item, with the bar's range and the click, so the host can open a menu on them.
33
+ * On the overlaid group view the bar is the bucket across every group, or across the one the legend has emphasised
34
+ * (`group` then says which): there is no one bar to click, so the column is the target.
35
+ */
36
+ onSelectBucket?: (sel: {
37
+ testNumber: number;
38
+ low: number;
39
+ high: number;
40
+ items: Array<{
41
+ item: HistogramItem;
42
+ dies: Die[];
43
+ }>;
44
+ group?: string;
45
+ }, e: MouseEvent) => void;
30
46
  /** Document to build this panel's DOM into. Default `document` — pass the
31
47
  * host's own `ownerDocument` when the container might live in a
32
48
  * different document (e.g. a gallery card detached into its own popup
@@ -1 +1 @@
1
- import{buildTestHistogramData as qt,collectTestValues as Ht,buildTestHistogramSeries as Jt,testValueExtent as At}from"../../stats/histogram.js";import{SPACE as lt,fontPx as R,FONT as Ct,CLR as $t}from"../toolbar.js";import{fmt as gt}from"../../renderer/fmt.js";import{niceStep as Zt,fitTicks as Et}from"../../renderer/axisTicks.js";import{QUANTITY as te,categorical as ee}from"./palette.js";import{cardShell as ne,observeResize as Wt,makeTooltip as ie,attachChartTip as oe,positionChartTooltip as Bt,makeLinkedTestSelect as se,makeWaferSelect as le,makeLinkedAxisPrefs as ae,renderEmptyState as yt,chartFillHeight as Dt,applyCanvasFlow as It,makeAxisFormat as Tt,horizontalTickSpacing as Pt,PADDING as mt,robustFence as Kt,shouldIncludeLimitsByDefault as Rt,drawOffAxisLimits as re,resolveAxisRange as ce,limitLines as Ot,limitExtent as de,hasBothLimitKinds as ue,stackLabelRows as me,strokeLimitLine as fe,chartSwatchCss as he,makeSeriesLegendItem as pe,prepareCanvas as Ft}from"./chartShell.js";import{escHtml as Mt,maxOf as ge}from"../../core/utils.js";const vt=230,Nt=36,xe=18;function zt(n){const C=new Set(n.map($=>$.kind)).size;return xe+Math.max(0,C-1)*(R(-1)+2)}function Gt(n,C,$,G,et,E,s){n.font=`${R(-1)}px system-ui, sans-serif`;const h=C.filter(i=>!$.has(i.value)).map(i=>{const b=G(i.value),W=n.measureText(i.label).width;return{l:i,x:b,start:b-W/2,end:b+W/2}}),U=me(h),nt=R(-1)+2;h.forEach(({l:i,x:b},W)=>{fe(n,i,s,b,et,b,E),n.fillStyle=s,n.textAlign="center",n.textBaseline="bottom",n.fillText(i.label,b,et-2-U[W]*nt)})}function Vt(n,C,$,G,et,E,s,h=4){const U=Math.max(1,Zt(E/h)),nt=G-$;n.save(),n.font=`${R(-1)}px system-ui, sans-serif`,n.textAlign="right",n.textBaseline="middle";for(let i=0;i<=E+1e-9;i+=U){const b=G-i/(E||1)*nt;n.strokeStyle=s.grid,n.lineWidth=1,n.beginPath(),n.moveTo(C,b),n.lineTo(C+et,b),n.stroke(),n.fillStyle=s.text,n.fillText(String(Math.round(i)),C-4,b)}n.strokeStyle=s.axis,n.lineWidth=1,n.beginPath(),n.moveTo(C,$),n.lineTo(C,G),n.stroke(),n.restore()}export function renderHistogramPanel(n){const{title:C="Test value distribution",items:$,testDefs:G,onSaveImage:et,groups:E}=n,{card:s,body:h,controlsRow:U}=ne(C,et,n.ownerDocument),nt=G.filter(a=>a.testNumber!==void 0);let i=n.selectedTestNumber??nt[0]?.testNumber??null,b=null,W=0;const kt=se(nt,i,a=>{i=a,q(),n.onTestChange?.(a)},{maxWidth:"200px",ownerDocument:s.ownerDocument}),jt=kt.el;U.appendChild(jt);const ft=$.length>1?le($,b,a=>{b=a,q()},{ownerDocument:s.ownerDocument}):null;ft&&U.appendChild(ft);function _t(a){return y===a||a!==null&&!(E??[]).some(T=>T.key===a)?!1:(y=a,q(),!0)}const xt=s.ownerDocument.createElement("span");Object.assign(xt.style,{display:"inline-flex",gap:lt.lg,alignItems:"center"}),U.appendChild(xt);const at=ae(xt,n.axisPrefs,a=>{n.onAxisPrefsChange?.(a),q()},s.ownerDocument),B=ie(s);let rt=null,y=null;function it(a){const T=G.find(D=>D.testNumber===a),V=Ot(T,at.get().limits),{lo:w,hi:M}=de(V);return{unit:T?.unit,lines:V,limitLow:w,limitHigh:M,hasLimits:Ot(T).length>0,hasBothKinds:ue(T)}}function q(){const{includeLimits:a,clipOutliers:T}=at.get();if(rt?.disconnect(),rt=null,h.innerHTML="",i===null){yt(h,"No parametric test data available for a histogram.");return}const V=!!E&&E.length>0;let w=!1,M;if(V){const{limitLow:d,limitHigh:g}=it(i),x=E.flatMap(l=>l.items),{min:e,max:f}=At(x,i);if(w=a??Rt(e,f,d,g),T){const l=Ht(x,i),u=Kt(l);u&&(M={lo:Math.max(e,u.lo),hi:Math.min(f,u.hi)},W=l.reduce((Z,K)=>Z+(K<M.lo||K>M.hi?1:0),0))}M||(W=0)}const D=V?Jt(E,i,16,w?it(i).limitLow:void 0,w?it(i).limitHigh:void 0,M):null;if(ft&&(ft.style.display=D?"none":""),D){const{hasLimits:d,hasBothKinds:g}=it(i);if(at.sync(w,d,g),D.series.length===0){yt(h,"No parametric test data available for a histogram.");return}Xt(D);return}const j=b!==null?[$[b]]:$,{unit:I,lines:_,limitLow:S,limitHigh:X,hasLimits:ct,hasBothKinds:ht}=it(i),dt=Ht(j,i),{min:ut,max:pt}=At(j,i),P=a??Rt(ut,pt,S,X);at.sync(P,ct,ht);const ot=T?Kt(dt):null,O=ot?{lo:Math.max(ut,ot.lo),hi:Math.min(pt,ot.hi)}:void 0;W=O?dt.filter(d=>d<O.lo||d>O.hi).length:0;const H=qt(j,i,16,P?S:void 0,P?X:void 0,O);if(H.length===0){yt(h,"No parametric test data available for a histogram.");return}const o=Math.max(ge(H.map(d=>d.count)),1),p=s.ownerDocument.createElement("div");Object.assign(p.style,{fontSize:Ct.body,color:$t.label,marginBottom:lt.xxs}),p.textContent=`max ${o} dies/bucket`+(W?` \xB7 ${W} value${W===1?"":"s"} outside clipped range`:""),h.appendChild(p);const r=s.ownerDocument.createElement("canvas");r.style.display="block",r.style.cursor="default",h.appendChild(r);let t=-1;const c=H[0].rangeLow,m=H[H.length-1].rangeHigh,v=m-c||1,F=Math.max(Math.abs(c),Math.abs(m));function J(d){const g=mt+36,x=r.clientWidth-g-mt,e=zt(_),f=d-Nt-e;return{plotX:g,plotMaxWidth:Math.max(10,x),plotMaxHeight:f,plotTop:e}}function k(d){const{plotX:g,plotMaxWidth:x}=J(vt);if(d<g||d>g+x)return-1;const e=Math.floor((d-g)/x*H.length);return e>=0&&e<H.length?e:-1}function A(){It(r,p);const d=h.clientWidth,g=Dt(s,h,r,vt),x=Ft(r,s,d,g);if(!x)return;const{ctx:e,theme:f}=x,{plotX:l,plotMaxWidth:u,plotMaxHeight:Z,plotTop:K}=J(g),L=K+Z,tt=u/H.length;Vt(e,l,K,L,u,o,{text:f.textMuted,axis:f.border,grid:f.grid}),H.forEach((z,Q)=>{const wt=z.count/o*Z,Lt=l+Q*tt,St=L-wt;e.fillStyle=Q===t?f.text:te,e.globalAlpha=Q===t?1:.7,e.fillRect(Lt+1,St,Math.max(1,tt-2),wt),e.globalAlpha=1,Q===t&&(e.fillStyle=f.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${z.count}`,Lt+tt/2,St-2))});const st=z=>l+(z-c)/v*u;e.font=`${R(-1)}px system-ui, sans-serif`;const{offAxis:N}=ce({dataMin:c,dataMax:m,limits:_,includeLimits:!1});re(e,N,{left:l,right:l+u,top:K,bottom:L},"horizontal",f.limitLine,Tt(F,I).tick),Gt(e,_,new Set(N.map(z=>z.value)),st,K,L,f.limitLine),e.font=`${R(-1)}px system-ui, sans-serif`,e.strokeStyle=f.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,L),e.lineTo(l+u,L),e.stroke(),e.fillStyle=f.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:Y,step:Ut}=Et(c,m,u,Pt(e,F,I)),bt=Tt(F,I,Ut||void 0);for(const z of Y){const Q=l+(z-c)/v*u;e.beginPath(),e.moveTo(Q,L),e.lineTo(Q,L+4),e.stroke(),e.fillText(bt.tick(z),Q,L+6)}bt.unitLabel&&(e.save(),e.font=`${R(-1)}px system-ui, sans-serif`,e.fillStyle=f.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${bt.unitLabel})`,l+u,L+20),e.restore())}r.addEventListener("mousemove",d=>{const g=r.getBoundingClientRect(),x=k(d.clientX-g.left);if(x!==t&&(t=x,A()),x>=0){const e=H[x];B.innerHTML=`<strong>${Mt(`${gt(e.rangeLow,I,"engineering")} \u2013 ${gt(e.rangeHigh,I,"engineering")}`)}</strong><br>${e.count} dies`,B.style.display="block",Bt(B,s,d.clientX,d.clientY)}else B.style.display="none"}),r.addEventListener("mouseleave",()=>{t!==-1&&(t=-1,A()),B.style.display="none"}),rt=Wt(s,()=>A()),A()}function Xt(a){const{unit:T,lines:V}=it(i),w=a.ranges,M=a.series,D=ee;y&&!M.some(o=>o.groupKey===y)&&(y=null);const j=Math.max(1,...M.flatMap(o=>o.counts)),I=s.ownerDocument.createElement("div");Object.assign(I.style,{fontSize:Ct.body,color:$t.label,marginBottom:lt.xxs}),I.textContent=`${M.length} groups \xB7 max ${j} dies/bucket`,h.appendChild(I);const _=s.ownerDocument.createElement("div");Object.assign(_.style,{display:"flex",flexWrap:"wrap",gap:lt.sm,marginTop:lt.md,marginBottom:lt.xl}),M.forEach((o,p)=>{const r=pe(o.groupKey,D(p),s.ownerDocument),t=r.el;oe(t,s,B,`${o.groupKey} \u2014 click to emphasize (dim the rest)`),r.setState({selected:y===o.groupKey,dimmed:y!==null&&y!==o.groupKey}),t.addEventListener("click",()=>{const c=y===o.groupKey?null:o.groupKey;_t(c),n.onGroupChange?.(c)}),_.appendChild(t)}),h.appendChild(_);const S=s.ownerDocument.createElement("canvas");S.style.display="block",S.style.cursor="default",h.appendChild(S);const X=w[0].rangeLow,ct=w[w.length-1].rangeHigh,ht=ct-X||1,dt=Math.max(Math.abs(X),Math.abs(ct)),ut=o=>{const p=(o.ownerDocument.defaultView??window).getComputedStyle(o);return o.offsetHeight+(parseFloat(p.marginTop)||0)+(parseFloat(p.marginBottom)||0)},pt=()=>ut(I)+ut(_);let P=-1,ot={plotX:mt+36,plotMaxWidth:10,barWidth:10};function O(){It(S,pt());const o=h.clientWidth,p=Dt(s,h,S,vt),r=Ft(S,s,o,p);if(!r)return;const{ctx:t,theme:c}=r,m=mt+36,v=Math.max(10,o-m-mt),F=zt(V),J=p-Nt-F,k=F+J,A=v/w.length;ot={plotX:m,plotMaxWidth:v,barWidth:A},Vt(t,m,F,k,v,j,{text:c.textMuted,axis:c.border,grid:c.grid}),P>=0&&(t.fillStyle=c.bgHover,t.globalAlpha=.5,t.fillRect(m+P*A,F,A,J),t.globalAlpha=1);const d=M.map((l,u)=>({s:l,i:u})).sort((l,u)=>(l.s.groupKey===y?1:0)-(u.s.groupKey===y?1:0));for(const{s:l,i:u}of d){const Z=y===l.groupKey,K=y!==null&&!Z,L=D(u);t.strokeStyle=L,t.fillStyle=L,t.lineWidth=Z?2:1,t.globalAlpha=K?.12:y?.32:.22,t.beginPath(),t.moveTo(m,k),l.counts.forEach((tt,st)=>{const N=m+st*A,Y=k-tt/j*J;t.lineTo(N,Y),t.lineTo(N+A,Y)}),t.lineTo(m+v,k),t.closePath(),t.fill(),t.globalAlpha=K?.3:1,t.beginPath(),l.counts.forEach((tt,st)=>{const N=m+st*A,Y=k-tt/j*J;st===0?t.moveTo(N,Y):t.lineTo(N,Y),t.lineTo(N+A,Y)}),t.stroke()}t.globalAlpha=1;const g=l=>m+(l-X)/ht*v;t.font=`${R(-1)}px system-ui, sans-serif`,Gt(t,V,new Set,g,F,k,c.limitLine),t.font=`${R(-1)}px system-ui, sans-serif`,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.moveTo(m,k),t.lineTo(m+v,k),t.stroke(),t.fillStyle=c.textMuted,t.textAlign="center",t.textBaseline="top";const{ticks:x,step:e}=Et(X,ct,v,Pt(t,dt,T)),f=Tt(dt,T,e||void 0);for(const l of x){const u=m+(l-X)/ht*v;t.beginPath(),t.moveTo(u,k),t.lineTo(u,k+4),t.stroke(),t.fillText(f.tick(l),u,k+6)}f.unitLabel&&(t.save(),t.font=`${R(-1)}px system-ui, sans-serif`,t.fillStyle=c.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${f.unitLabel})`,m+v,k+20),t.restore())}function H(o){const{plotX:p,plotMaxWidth:r,barWidth:t}=ot;if(o<p||o>p+r)return-1;const c=Math.floor((o-p)/t);return c>=0&&c<w.length?c:-1}S.addEventListener("mousemove",o=>{const p=S.getBoundingClientRect(),r=H(o.clientX-p.left);if(r!==P&&(P=r,O()),r>=0){const t=w[r],c=M.map((m,v)=>`<span style="${he(D(v))};margin-right:4px"></span>${Mt(`${m.groupKey}: ${m.counts[r]}`)}`).join("<br>");B.innerHTML=`<strong>${Mt(`${gt(t.rangeLow,T,"engineering")} \u2013 ${gt(t.rangeHigh,T,"engineering")}`)}</strong><br>${c}`,B.style.display="block",Bt(B,s,o.clientX,o.clientY)}else B.style.display="none"}),S.addEventListener("mouseleave",()=>{P!==-1&&(P=-1,O()),B.style.display="none"}),rt=Wt(s,()=>O()),O()}q();function Yt(a){kt.set(a)&&(i=a,q())}function Qt(a){at.set(a)&&q()}return{card:s,setTest:Yt,setAxisPrefs:Qt,destroy:()=>rt?.disconnect()}}
1
+ import{buildTestHistogramData as Zt,collectTestValues as Ct,buildTestHistogramSeries as te,testValueExtent as Bt,diesInBucket as At}from"../../stats/histogram.js";import{SPACE as lt,fontPx as N,FONT as $t,CLR as Et}from"../toolbar.js";import{fmt as xt}from"../../renderer/fmt.js";import{niceStep as ee,fitTicks as Wt}from"../../renderer/axisTicks.js";import{QUANTITY as ne,categorical as ie}from"./palette.js";import{cardShell as oe,observeResize as Dt,makeTooltip as se,attachChartTip as le,positionChartTooltip as It,makeLinkedTestSelect as ae,makeWaferSelect as re,makeLinkedAxisPrefs as ce,renderEmptyState as kt,chartFillHeight as Pt,applyCanvasFlow as Rt,makeAxisFormat as Tt,horizontalTickSpacing as Kt,PADDING as mt,robustFence as Nt,shouldIncludeLimitsByDefault as Ot,drawOffAxisLimits as ue,resolveAxisRange as de,limitLines as Ft,limitExtent as me,hasBothLimitKinds as he,stackLabelRows as fe,strokeLimitLine as ge,chartSwatchCss as pe,makeSeriesLegendItem as xe,prepareCanvas as zt}from"./chartShell.js";import{escHtml as wt,maxOf as be}from"../../core/utils.js";const Mt=230,Gt=36,ye=18;function Vt(n){const $=new Set(n.map(E=>E.kind)).size;return ye+Math.max(0,$-1)*(N(-1)+2)}function jt(n,$,E,V,et,S,l){n.font=`${N(-1)}px system-ui, sans-serif`;const x=$.filter(i=>!E.has(i.value)).map(i=>{const k=V(i.value),W=n.measureText(i.label).width;return{l:i,x:k,start:k-W/2,end:k+W/2}}),U=fe(x),nt=N(-1)+2;x.forEach(({l:i,x:k},W)=>{ge(n,i,l,k,et,k,S),n.fillStyle=l,n.textAlign="center",n.textBaseline="bottom",n.fillText(i.label,k,et-2-U[W]*nt)})}function Xt(n,$,E,V,et,S,l,x=4){const U=Math.max(1,ee(S/x)),nt=V-E;n.save(),n.font=`${N(-1)}px system-ui, sans-serif`,n.textAlign="right",n.textBaseline="middle";for(let i=0;i<=S+1e-9;i+=U){const k=V-i/(S||1)*nt;n.strokeStyle=l.grid,n.lineWidth=1,n.beginPath(),n.moveTo($,k),n.lineTo($+et,k),n.stroke(),n.fillStyle=l.text,n.fillText(String(Math.round(i)),$-4,k)}n.strokeStyle=l.axis,n.lineWidth=1,n.beginPath(),n.moveTo($,E),n.lineTo($,V),n.stroke(),n.restore()}export function renderHistogramPanel(n){const{title:$="Test value distribution",items:E,testDefs:V,onSaveImage:et,groups:S}=n,{card:l,body:x,controlsRow:U}=oe($,et,n.ownerDocument),nt=V.filter(c=>c.testNumber!==void 0);let i=n.selectedTestNumber??nt[0]?.testNumber??null,k=null,W=0,ht;const vt=ae(nt,i,c=>{i=c,q(),n.onTestChange?.(c)},{maxWidth:"200px",ownerDocument:l.ownerDocument}),_t=vt.el;U.appendChild(_t);const ft=E.length>1?re(E,k,c=>{k=c,q()},{ownerDocument:l.ownerDocument}):null;ft&&U.appendChild(ft);function Yt(c){return b===c||c!==null&&!(S??[]).some(L=>L.key===c)?!1:(b=c,q(),!0)}const bt=l.ownerDocument.createElement("span");Object.assign(bt.style,{display:"inline-flex",gap:lt.lg,alignItems:"center"}),U.appendChild(bt);const at=ce(bt,n.axisPrefs,c=>{n.onAxisPrefsChange?.(c),q()},l.ownerDocument),C=se(l);let rt=null,b=null;function it(c){const L=V.find(I=>I.testNumber===c),j=Ft(L,at.get().limits),{lo:T,hi:w}=me(j);return{unit:L?.unit,lines:j,limitLow:T,limitHigh:w,hasLimits:Ft(L).length>0,hasBothKinds:he(L)}}function q(){const{includeLimits:c,clipOutliers:L}=at.get();if(rt?.disconnect(),rt=null,x.innerHTML="",i===null){kt(x,"No parametric test data available for a histogram.");return}const j=!!S&&S.length>0;let T=!1,w;if(j){const{limitLow:u,limitHigh:m}=it(i),g=S.flatMap(r=>r.items),{min:e,max:p}=Bt(g,i);if(T=c??Ot(e,p,u,m),L){const r=Ct(g,i),h=Nt(r);h&&(w={lo:Math.max(e,h.lo),hi:Math.min(p,h.hi)},W=r.reduce((Z,K)=>Z+(K<w.lo||K>w.hi?1:0),0))}w||(W=0),ht=w}const I=j?te(S,i,16,T?it(i).limitLow:void 0,T?it(i).limitHigh:void 0,w):null;if(ft&&(ft.style.display=I?"none":""),I){const{hasLimits:u,hasBothKinds:m}=it(i);if(at.sync(T,u,m),I.series.length===0){kt(x,"No parametric test data available for a histogram.");return}Qt(I);return}const O=k!==null?[E[k]]:E,{unit:D,lines:X,limitLow:M,limitHigh:_,hasLimits:ct,hasBothKinds:gt}=it(i),ut=Ct(O,i),{min:dt,max:pt}=Bt(O,i),P=c??Ot(dt,pt,M,_);at.sync(P,ct,gt);const ot=L?Nt(ut):null,R=ot?{lo:Math.max(dt,ot.lo),hi:Math.min(pt,ot.hi)}:void 0;W=R?ut.filter(u=>u<R.lo||u>R.hi).length:0,ht=R;const y=Zt(O,i,16,P?M:void 0,P?_:void 0,R);if(y.length===0){kt(x,"No parametric test data available for a histogram.");return}const o=Math.max(be(y.map(u=>u.count)),1),d=l.ownerDocument.createElement("div");d.dataset.wmapCaption="1",Object.assign(d.style,{fontSize:$t.body,color:Et.label,marginBottom:lt.xxs}),d.textContent=`max ${o} dies/bucket`+(W?` \xB7 ${W} value${W===1?"":"s"} outside clipped range`:"")+(n.onSelectBucket?" \xB7 click a bar to chart or tabulate its dies":""),x.appendChild(d);const s=l.ownerDocument.createElement("canvas");s.style.display="block",s.style.cursor="default",x.appendChild(s);let t=-1;const a=y[0].rangeLow,f=y[y.length-1].rangeHigh,H=f-a||1,F=Math.max(Math.abs(a),Math.abs(f));function J(u){const m=mt+36,g=s.clientWidth-m-mt,e=Vt(X),p=u-Gt-e;return{plotX:m,plotMaxWidth:Math.max(10,g),plotMaxHeight:p,plotTop:e}}function v(u){const{plotX:m,plotMaxWidth:g}=J(Mt);if(u<m||u>m+g)return-1;const e=Math.floor((u-m)/g*y.length);return e>=0&&e<y.length?e:-1}function A(){Rt(s,d);const u=x.clientWidth,m=Pt(l,x,s,Mt),g=zt(s,l,u,m);if(!g)return;const{ctx:e,theme:p}=g,{plotX:r,plotMaxWidth:h,plotMaxHeight:Z,plotTop:K}=J(m),B=K+Z,tt=h/y.length;Xt(e,r,K,B,h,o,{text:p.textMuted,axis:p.border,grid:p.grid}),y.forEach((G,Q)=>{const St=G.count/o*Z,Lt=r+Q*tt,Ht=B-St;e.fillStyle=Q===t?p.text:ne,e.globalAlpha=Q===t?1:.7,e.fillRect(Lt+1,Ht,Math.max(1,tt-2),St),e.globalAlpha=1,Q===t&&(e.fillStyle=p.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${G.count}`,Lt+tt/2,Ht-2))});const st=G=>r+(G-a)/H*h;e.font=`${N(-1)}px system-ui, sans-serif`;const{offAxis:z}=de({dataMin:a,dataMax:f,limits:X,includeLimits:!1});ue(e,z,{left:r,right:r+h,top:K,bottom:B},"horizontal",p.limitLine,Tt(F,D).tick),jt(e,X,new Set(z.map(G=>G.value)),st,K,B,p.limitLine),e.font=`${N(-1)}px system-ui, sans-serif`,e.strokeStyle=p.border,e.lineWidth=1,e.beginPath(),e.moveTo(r,B),e.lineTo(r+h,B),e.stroke(),e.fillStyle=p.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:Y,step:Jt}=Wt(a,f,h,Kt(e,F,D)),yt=Tt(F,D,Jt||void 0);for(const G of Y){const Q=r+(G-a)/H*h;e.beginPath(),e.moveTo(Q,B),e.lineTo(Q,B+4),e.stroke(),e.fillText(yt.tick(G),Q,B+6)}yt.unitLabel&&(e.save(),e.font=`${N(-1)}px system-ui, sans-serif`,e.fillStyle=p.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${yt.unitLabel})`,r+h,B+20),e.restore())}s.addEventListener("mousemove",u=>{const m=s.getBoundingClientRect(),g=v(u.clientX-m.left);if(g!==t&&(t=g,A()),g>=0){const e=y[g];C.innerHTML=`<strong>${wt(`${xt(e.rangeLow,D,"engineering")} \u2013 ${xt(e.rangeHigh,D,"engineering")}`)}</strong><br>${e.count} dies`+(n.onSelectBucket&&e.count>0?"<br><em>click to chart or tabulate these dies</em>":""),C.style.display="block",It(C,l,u.clientX,u.clientY)}else C.style.display="none"}),s.addEventListener("mouseleave",()=>{t!==-1&&(t=-1,A()),C.style.display="none"}),n.onSelectBucket&&(s.style.cursor="pointer",s.addEventListener("click",u=>{const m=v(u.clientX-s.getBoundingClientRect().left);if(m<0||i===null||y[m].count===0)return;const g=At(O,i,y,m,ht);g.length!==0&&(C.style.display="none",n.onSelectBucket({testNumber:i,low:y[m].rangeLow,high:y[m].rangeHigh,items:g},u))})),rt=Dt(l,()=>A()),A()}function Qt(c){const{unit:L,lines:j}=it(i),T=c.ranges,w=c.series,I=ie;b&&!w.some(o=>o.groupKey===b)&&(b=null);const O=Math.max(1,...w.flatMap(o=>o.counts)),D=l.ownerDocument.createElement("div");D.dataset.wmapCaption="1",Object.assign(D.style,{fontSize:$t.body,color:Et.label,marginBottom:lt.xxs}),D.textContent=`${w.length} groups \xB7 max ${O} dies/bucket`+(n.onSelectBucket?" \xB7 click a column to chart or tabulate its dies":""),x.appendChild(D);const X=l.ownerDocument.createElement("div");Object.assign(X.style,{display:"flex",flexWrap:"wrap",gap:lt.sm,marginTop:lt.md,marginBottom:lt.xl}),w.forEach((o,d)=>{const s=xe(o.groupKey,I(d),l.ownerDocument),t=s.el;le(t,l,C,`${o.groupKey} \u2014 click to emphasize (dim the rest)`),s.setState({selected:b===o.groupKey,dimmed:b!==null&&b!==o.groupKey}),t.addEventListener("click",()=>{const a=b===o.groupKey?null:o.groupKey;Yt(a),n.onGroupChange?.(a)}),X.appendChild(t)}),x.appendChild(X);const M=l.ownerDocument.createElement("canvas");M.style.display="block",M.style.cursor="default",x.appendChild(M);const _=T[0].rangeLow,ct=T[T.length-1].rangeHigh,gt=ct-_||1,ut=Math.max(Math.abs(_),Math.abs(ct)),dt=o=>{const d=(o.ownerDocument.defaultView??window).getComputedStyle(o);return o.offsetHeight+(parseFloat(d.marginTop)||0)+(parseFloat(d.marginBottom)||0)},pt=()=>dt(D)+dt(X);let P=-1,ot={plotX:mt+36,plotMaxWidth:10,barWidth:10};function R(){Rt(M,pt());const o=x.clientWidth,d=Pt(l,x,M,Mt),s=zt(M,l,o,d);if(!s)return;const{ctx:t,theme:a}=s,f=mt+36,H=Math.max(10,o-f-mt),F=Vt(j),J=d-Gt-F,v=F+J,A=H/T.length;ot={plotX:f,plotMaxWidth:H,barWidth:A},Xt(t,f,F,v,H,O,{text:a.textMuted,axis:a.border,grid:a.grid}),P>=0&&(t.fillStyle=a.bgHover,t.globalAlpha=.5,t.fillRect(f+P*A,F,A,J),t.globalAlpha=1);const u=w.map((r,h)=>({s:r,i:h})).sort((r,h)=>(r.s.groupKey===b?1:0)-(h.s.groupKey===b?1:0));for(const{s:r,i:h}of u){const Z=b===r.groupKey,K=b!==null&&!Z,B=I(h);t.strokeStyle=B,t.fillStyle=B,t.lineWidth=Z?2:1,t.globalAlpha=K?.12:b?.32:.22,t.beginPath(),t.moveTo(f,v),r.counts.forEach((tt,st)=>{const z=f+st*A,Y=v-tt/O*J;t.lineTo(z,Y),t.lineTo(z+A,Y)}),t.lineTo(f+H,v),t.closePath(),t.fill(),t.globalAlpha=K?.3:1,t.beginPath(),r.counts.forEach((tt,st)=>{const z=f+st*A,Y=v-tt/O*J;st===0?t.moveTo(z,Y):t.lineTo(z,Y),t.lineTo(z+A,Y)}),t.stroke()}t.globalAlpha=1;const m=r=>f+(r-_)/gt*H;t.font=`${N(-1)}px system-ui, sans-serif`,jt(t,j,new Set,m,F,v,a.limitLine),t.font=`${N(-1)}px system-ui, sans-serif`,t.strokeStyle=a.border,t.lineWidth=1,t.beginPath(),t.moveTo(f,v),t.lineTo(f+H,v),t.stroke(),t.fillStyle=a.textMuted,t.textAlign="center",t.textBaseline="top";const{ticks:g,step:e}=Wt(_,ct,H,Kt(t,ut,L)),p=Tt(ut,L,e||void 0);for(const r of g){const h=f+(r-_)/gt*H;t.beginPath(),t.moveTo(h,v),t.lineTo(h,v+4),t.stroke(),t.fillText(p.tick(r),h,v+6)}p.unitLabel&&(t.save(),t.font=`${N(-1)}px system-ui, sans-serif`,t.fillStyle=a.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${p.unitLabel})`,f+H,v+20),t.restore())}function y(o){const{plotX:d,plotMaxWidth:s,barWidth:t}=ot;if(o<d||o>d+s)return-1;const a=Math.floor((o-d)/t);return a>=0&&a<T.length?a:-1}M.addEventListener("mousemove",o=>{const d=M.getBoundingClientRect(),s=y(o.clientX-d.left);if(s!==P&&(P=s,R()),s>=0){const t=T[s],a=w.map((f,H)=>`<span style="${pe(I(H))};margin-right:4px"></span>${wt(`${f.groupKey}: ${f.counts[s]}`)}`).join("<br>");C.innerHTML=`<strong>${wt(`${xt(t.rangeLow,L,"engineering")} \u2013 ${xt(t.rangeHigh,L,"engineering")}`)}</strong><br>${a}`,C.style.display="block",It(C,l,o.clientX,o.clientY)}else C.style.display="none"}),M.addEventListener("mouseleave",()=>{P!==-1&&(P=-1,R()),C.style.display="none"}),n.onSelectBucket&&S&&(M.style.cursor="pointer",M.addEventListener("click",o=>{const d=y(o.clientX-M.getBoundingClientRect().left);if(d<0||i===null)return;const s=b?S.filter(a=>a.key===b):S,t=At(s.flatMap(a=>a.items),i,T,d,ht);t.length!==0&&(C.style.display="none",n.onSelectBucket({testNumber:i,low:T[d].rangeLow,high:T[d].rangeHigh,items:t,...b?{group:b}:{}},o))})),rt=Dt(l,()=>R()),R()}q();function Ut(c){vt.set(c)&&(i=c,q())}function qt(c){at.set(c)&&q()}return{card:l,setTest:Ut,setAxisPrefs:qt,destroy:()=>rt?.disconnect()}}
@@ -1,6 +1,8 @@
1
1
  /** Neutral single fill for "how much" encodings (bars, boxes, histogram). */
2
2
  export declare const QUANTITY = "#4e79a7";
3
3
  export declare function categorical(i: number): string;
4
+ /** How many series colours are distinct from one another before the palette repeats. */
5
+ export declare const CATEGORICAL_COUNT: number;
4
6
  /**
5
7
  * Sequential fill for a yield percentage on a **fixed** domain — light at
6
8
  * ≤50%, deepest at 100% — so the same yield is always the same colour
@@ -1 +1 @@
1
- import{clamp01 as p}from"../../core/utils.js";export const QUANTITY="#4e79a7";const n=["#0072B2","#E69F00","#009E73","#CC79A7","#56B4E9","#D55E00","#8C6D31","#999999"];export function categorical(t){return n[(t%n.length+n.length)%n.length]}function E(t,e,c){const s=[parseInt(t.slice(1,3),16),parseInt(t.slice(3,5),16),parseInt(t.slice(5,7),16)],l=[parseInt(e.slice(1,3),16),parseInt(e.slice(3,5),16),parseInt(e.slice(5,7),16)],r=s.map((o,i)=>Math.round(o+(l[i]-o)*c));return`rgb(${r[0]},${r[1]},${r[2]})`}export function yieldFill(t){const e=p((t-50)/50);return E("#e7eef6","#2f6395",e)}export function capabilityColor(t){return t===null?"#999999":t>=1.33?"#009E73":t>=1?"#E69F00":"#D55E00"}export const CORRELATION_POSITIVE="#0072B2",CORRELATION_NEGATIVE="#D55E00";
1
+ import{clamp01 as i}from"../../core/utils.js";export const QUANTITY="#4e79a7";const E=["#0072B2","#E69F00","#009E73","#CC79A7","#56B4E9","#D55E00","#8C6D31","#999999"],p=["#1f1ff9","#f91ff9","#05d105","#65188b","#f91f68","#607afb","#8b1818","#1f4951"],n=[...E,...p];export function categorical(t){return n[(t%n.length+n.length)%n.length]}export const CATEGORICAL_COUNT=n.length;function I(t,e,c){const s=[parseInt(t.slice(1,3),16),parseInt(t.slice(3,5),16),parseInt(t.slice(5,7),16)],l=[parseInt(e.slice(1,3),16),parseInt(e.slice(3,5),16),parseInt(e.slice(5,7),16)],r=s.map((o,f)=>Math.round(o+(l[f]-o)*c));return`rgb(${r[0]},${r[1]},${r[2]})`}export function yieldFill(t){const e=i((t-50)/50);return I("#e7eef6","#2f6395",e)}export function capabilityColor(t){return t===null?"#999999":t>=1.33?"#009E73":t>=1?"#E69F00":"#D55E00"}export const CORRELATION_POSITIVE="#0072B2",CORRELATION_NEGATIVE="#D55E00";