@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
@@ -0,0 +1,27 @@
1
+ import { type PlotPoint, type ResolvedPlot } from '../../stats/plotData.js';
2
+ import { type SaveImageHandler } from './chartShell.js';
3
+ export interface PlotChartOptions {
4
+ ownerDocument?: Document;
5
+ onSaveImage?: SaveImageHandler;
6
+ /** The label of the wafer an item index names, for tooltips. */
7
+ waferLabel: (item: number) => string;
8
+ /** Click a point: open its wafer, on `testNumber` (the plot's own measurement) when it has one. Omit where there is no wafer to open. */
9
+ onOpenWafer?: (item: number, testNumber?: number) => void;
10
+ /** What a click on a point does, in the reader's words. Default `open this wafer`. */
11
+ openActionLabel?: string;
12
+ /** Drag a rectangle over a scatter: the dies inside it, the pointer position and the canvas to anchor a menu on. */
13
+ onSelectPoints?: (points: PlotPoint[], at: {
14
+ x: number;
15
+ y: number;
16
+ }, anchor: HTMLElement) => void;
17
+ }
18
+ export interface PlotChartHandle {
19
+ card: HTMLElement;
20
+ /** The row under the title, for the host's own buttons (Edit, Duplicate, Delete). */
21
+ actions: HTMLElement;
22
+ /** Draw `resolved`, replacing whatever was shown. */
23
+ setPlot(resolved: ResolvedPlot): void;
24
+ destroy(): void;
25
+ }
26
+ export declare function renderPlotChart(options: PlotChartOptions): PlotChartHandle;
27
+ //# sourceMappingURL=plotChart.d.ts.map
@@ -0,0 +1 @@
1
+ import{escHtml as I,maxOf as fe,minOf as We}from"../../core/utils.js";import{fiveNumberSummary as Be}from"../../stats/math.js";import{fmt as R}from"../../renderer/fmt.js";import{fitTicks as Te}from"../../renderer/axisTicks.js";import{plotFootnote as Fe,plotTestNumber as Oe,plotTitle as et}from"../../stats/plotData.js";import{SPACE as ee,RADIUS as tt,fontPx as me,FONT as we,CLR as xe,markNoPrint as ze}from"../toolbar.js";import{cardShell as nt,observeResize as ot,makeTooltip as it,attachChartTip as lt,chartFillHeight as st,applyCanvasFlow as ke,prepareCanvas as Me,positionChartTooltip as Le,resolveChartCanvasColors as ve,makeAxisFormat as at,horizontalTickSpacing as rt,VERTICAL_TICK_SPACING_PX as ct,makeSeriesLegendItem as ft,renderEmptyState as Re,limitLines as mt,limitExtent as ht,stackLabelRows as qe,strokeLimitLine as He,drawOffAxisLimits as ut,limitLabelSide as dt,shouldIncludeLimitsByDefault as gt}from"./chartShell.js";import{categorical as pt}from"./palette.js";import{resolveValueColorFn as $e}from"../../renderer/colorSchemes.js";import{wirePointInteractions as yt}from"./pointInteractions.js";const M=64,Xe=16,k=12,he=52,bt=16,Pe=5e3;function te(b,P,$={}){const q=P.scale==="log";let w=b.length?We(b):0,O=b.length?fe(b):1;q&&(w=Math.log10(Math.max(w,Number.MIN_VALUE)),O=Math.log10(Math.max(O,Number.MIN_VALUE)));const ne=(O-w)*.05||(q?.5:1);w=$.floorAtZero&&!q&&w>=0?0:w-ne,O+=ne;const s=x=>x===void 0||!Number.isFinite(x)?void 0:q?x>0?Math.log10(x):void 0:x,g=s(P.min),H=s(P.max);g!==void 0&&(w=g),H!==void 0&&(O=H),O>w||(O=w+1);const C=O-w,V=!!P.reverse,Z=x=>{const Y=((q?Math.log10(Math.max(x,Number.MIN_VALUE)):x)-w)/C;return V?1-Y:Y};return{lo:q?10**w:w,hi:q?10**O:O,log:q,reverse:V,frac:Z}}function _e(b,P,$,q){if(P/b<10)return Te(b,P,$,q);const w=Math.floor(Math.log10(b)),O=Math.ceil(Math.log10(P)),ne=g=>{const H=[];for(let C=w;C<=O;C++)for(const V of g){const Z=V*10**C;Z>=b&&Z<=P&&H.push(Z)}return H},L=ne([1]);return{ticks:L.length>=4?L:ne([1,2,5]),step:0}}export function renderPlotChart(b){const P=b.ownerDocument??document,{card:$,heading:q,body:w,controlsRow:O,setPngDecor:ne}=nt("Plot",b.onSaveImage,P);$.dataset.wmapPlotCard="1";const L=it($);let s,g,H,C,V,Z=[];const x=new Set,Y=new Set;let ue=null,X=-1,ce=[],z,K,G,_=null,J=[],D=-1,de=he;const oe=e=>pt(e),ge=e=>s?.groups[e]||"";function pe(){const e=w.clientWidth,l=Math.max(220,Math.min(420,e*.62))+(de-he),c=st($,w,g,l);return{w:e,h:c,plotW:Math.max(10,e-M-Xe),plotH:Math.max(10,c-k-de)}}const ie=e=>mt(e?.limits,s?.spec.limits??"both").filter(l=>e?.scale!=="log"||l.value>0),ae=(e,l)=>{const c=e.filter(Number.isFinite);if(!l.length||c.length===0)return e;const{lo:r,hi:i}=ht(l);return gt(We(c),fe(c),r,i)?[...e,...l.map(o=>o.value)]:e};function re(e,l,c,r,i,o,t,n){if(o.length===0)return;const f=o.filter(h=>h.value>=i.lo&&h.value<=i.hi),p=o.filter(h=>!f.includes(h)).map(h=>({value:h.value,label:h.label,side:h.value<i.lo?"lo":"hi"}));e.save(),e.font=`${me(-1)}px system-ui, sans-serif`;const d=h=>`${h.label} ${R(h.value,t.unit)}`,a=me(-1)+2;if(n==="y"){const h=f.map(m=>{const y=k+(1-i.frac(m.value))*r;return{l:m,y,start:y-6-a/2,end:y-6+a/2}}),S=qe(h,1),u=h.length?fe(h.map(m=>e.measureText(d(m.l)).width))+8:0;h.forEach(({l:m,y},v)=>{e.globalAlpha=.7,He(e,m,l,M,y,M+c,y),e.globalAlpha=1,e.fillStyle=l,e.textAlign="left",e.textBaseline="alphabetic",e.fillText(d(m),M+3+S[v]*u,y-4)})}else{const h=f.map(u=>{const m=M+i.frac(u.value)*c,y=e.measureText(d(u)).width,v=dt(m,y,M,M+c,u.end==="lo");return{l:u,x:m,side:v,start:v<0?m-3-y:m+3,end:v<0?m-3:m+3+y}}),S=qe(h,4);h.forEach(({l:u,x:m,side:y},v)=>{e.globalAlpha=.7,He(e,u,l,m,k,m,k+r),e.globalAlpha=1,e.fillStyle=l,e.textAlign=y<0?"right":"left",e.textBaseline="top",e.fillText(d(u),m+y*3,k+2+S[v]*a)})}e.restore(),ut(e,p,{left:M,right:M+c,top:k,bottom:k+r},n==="y"?"vertical":"horizontal",l,h=>R(h,t.unit))}function Ee(e,l,c,r){return at(Math.max(Math.abs(l),Math.abs(c)),e.unit,r)}function Ce(e,l){return l?`${e.label} (${l})`:e.label}function ye(e,l,c,r,i,o,t,n,f,p){e.font=`${me(-1)}px system-ui, sans-serif`;const d=()=>ct,a=n.log?_e(n.lo,n.hi,o,d):Te(n.lo,n.hi,o,d),h=Ee(p,n.lo,n.hi,a.step||void 0);e.strokeStyle=l.grid,e.lineWidth=.5,e.fillStyle=l.textMuted;let S="";if("labels"in t){const u=t.labels.length;t.labels.forEach((m,y)=>{const v=M+(y+.5)/u*i,T=m.length>22?`${m.slice(0,21)}\u2026`:m;t.rotate?(e.save(),e.translate(v,k+o+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.fillText(T,0,0),e.restore()):(e.textAlign="center",e.textBaseline="top",e.fillText(T,v,k+o+4))})}else{const u=rt(e,Math.max(Math.abs(t.lo),Math.abs(t.hi)),f.unit),m=t.log?_e(t.lo,t.hi,i,u):Te(t.lo,t.hi,i,u),y=Ee(f,t.lo,t.hi,m.step||void 0);S=y.unitLabel;for(const v of m.ticks){const T=M+t.frac(v)*i;e.beginPath(),e.moveTo(T,k),e.lineTo(T,k+o),e.stroke(),e.textAlign="center",e.textBaseline="top",e.fillText(y.tick(v),T,k+o+4)}}for(const u of a.ticks){const m=k+(1-n.frac(u))*o;e.beginPath(),e.moveTo(M,m),e.lineTo(M+i,m),e.stroke(),e.textAlign="right",e.textBaseline="middle",e.fillText(h.tick(u),M-4,m)}e.font=`${me(0)}px system-ui, sans-serif`,e.fillStyle=l.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(Ce(f,S),M+i/2,r-6),e.save(),e.translate(14,k+o/2),e.rotate(-Math.PI/2),e.textBaseline="top",e.fillText(Ce(p,h.unitLabel),0,0),e.restore(),e.strokeStyle=l.border,e.lineWidth=1,e.beginPath(),e.moveTo(M,k),e.lineTo(M,k+o),e.moveTo(M,k+o),e.lineTo(M+i,k+o),e.stroke()}function je(){if(!s||!g||s.marks?.type!=="scatter"||!s.x||!s.y)return;ke(g,C??0);const e=ve($),{w:l,h:c,plotW:r,plotH:i}=pe(),o=Me(g,$,l,c);if(!o)return;const{ctx:t}=o,n=s.marks.points,f=x.size===0?n:n.filter(A=>x.has(A.group)),p=s.colorScale,d=$e(),a=ie(s.x),h=ie(s.y),S=te(ae(n.map(A=>A.x),a),s.x),u=te(ae(n.map(A=>A.y),h),s.y);z={x:S,y:u,plotW:r,plotH:i},ye(t,e,l,c,r,i,S,u,s.x,s.y),re(t,e.limitLine,r,i,S,a,s.x,"x"),re(t,e.limitLine,r,i,u,h,s.y,"y");const m=f.length>Pe?Math.ceil(f.length/Pe):1;t.globalAlpha=Math.max(.15,Math.min(.7,200/(f.length/m))),ce=[];const y=M,v=M+r,T=k,le=k+i;for(let A=0;A<f.length;A+=m){const N=f[A],se=M+S.frac(N.x)*r,U=k+(1-u.frac(N.y))*i;se<y||se>v||U<T||U>le||(ce.push({p:N,cx:se,cy:U}),t.beginPath(),t.arc(se,U,2.5,0,Math.PI*2),t.fillStyle=p&&N.value!==void 0?d((N.value-p.lo)/(p.hi-p.lo||1)):oe(N.group),t.fill())}t.globalAlpha=1;const E=ue?ce.find(A=>A.p===ue):void 0;if(E&&(t.beginPath(),t.arc(E.cx,E.cy,5,0,Math.PI*2),t.lineWidth=2,t.strokeStyle=e.text,t.stroke()),Y.size>0){t.lineWidth=1.5,t.strokeStyle=e.text;let A=0;for(const N of Y)if(!(x.size>0&&!x.has(N.group))){if(++A>Pe)break;t.beginPath(),t.arc(M+S.frac(N.x)*r,k+(1-u.frac(N.y))*i,4,0,Math.PI*2),t.stroke()}}}function Ue(){if(K=void 0,!s||s.marks?.type!=="histogram"||!s.x)return;const e=s.marks.values,l=e.flat();if(l.length===0)return;const c=te(ae(l,ie(s.x)),s.x),r=Math.max(1,s.spec.bins??bt),i=c.log?Math.log10(c.lo):c.lo,o=c.log?Math.log10(c.hi):c.hi,t=Array.from({length:r+1},(f,p)=>{const d=i+(o-i)*p/r;return c.log?10**d:d}),n=e.map(()=>new Array(r).fill(0));e.forEach((f,p)=>{for(const d of f){const a=c.log?d>0?Math.log10(d):NaN:d;if(!Number.isFinite(a)||a<i||a>o)continue;const h=Math.min(r-1,Math.floor((a-i)/(o-i)*r));n[p][h]++}}),K={edges:t,counts:n,maxCount:Math.max(1,...n.map(f=>fe(f)))}}function Ye(){if(!s||!g||!K||!s.x||!s.y)return;ke(g,C??0);const e=ve($),{w:l,h:c,plotW:r,plotH:i}=pe(),o=Me(g,$,l,c);if(!o)return;const{ctx:t}=o,{edges:n,counts:f,maxCount:p}=K,d=te([n[0],n[n.length-1]],{...s.x,min:n[0],max:n[n.length-1]}),a=te([0,p],{label:s.y.label,scale:"linear",min:0,max:p*1.05});z={x:d,y:a,plotW:r,plotH:i},ye(t,e,l,c,r,i,d,a,s.x,s.y),re(t,e.limitLine,r,i,d,ie(s.x),s.x,"x");const h=f.map((u,m)=>m).filter(u=>x.size===0||x.has(u)),S=h.length>1;for(const u of h)t.globalAlpha=S?.55:.85,t.fillStyle=oe(u),f[u].forEach((m,y)=>{if(m===0)return;const v=M+Math.min(d.frac(n[y]),d.frac(n[y+1]))*r,T=M+Math.max(d.frac(n[y]),d.frac(n[y+1]))*r,le=k+(1-a.frac(m))*i;t.fillRect(v,le,Math.max(1,T-v-1),k+i-le)});if(t.globalAlpha=1,X>=0){const u=M+Math.min(d.frac(n[X]),d.frac(n[X+1]))*r,m=M+Math.max(d.frac(n[X]),d.frac(n[X+1]))*r;t.lineWidth=1.5,t.strokeStyle=e.text,t.strokeRect(u,k,Math.max(1,m-u),i)}}function Ge(){const e=s?.marks;if(!s||!g||!s.x||!s.y||!e||e.type!=="box"&&e.type!=="bar")return;ke(g,C??0);const l=ve($),c=g.getContext("2d");let r=0;c&&(c.font=`${me(-1)}px system-ui, sans-serif`,r=fe(e.categories.map(E=>c.measureText(E.length>22?`${E.slice(0,21)}\u2026`:E).width)));const i=Math.max(10,w.clientWidth-M-Xe)/Math.max(1,e.categories.length),o=r+8>i;de=o?he+Math.min(110,r*.72):he;const{w:t,h:n,plotW:f,plotH:p}=pe(),d=Me(g,$,t,n);if(!d)return;const{ctx:a}=d,h=E=>E.filter(Number.isFinite),S=ie(s.y),u=te(ae(e.type==="bar"?[...h(e.values.flat()),0]:h(e.cells.flat(2)),S),s.y,{floorAtZero:e.type==="bar"});z={x:u,y:u,plotW:f,plotH:p},ye(a,l,t,n,f,p,{labels:e.categories,rotate:o},u,s.x,s.y),re(a,l.limitLine,f,p,u,S,s.y,"y");const m=e.categories.length,y=s.groups.length,v=f/m,T=v*.8/y;G={slotW:v,gw:T,n:m,groups:y,plotW:f};const le=u.log?k+p:k+(1-u.frac(0))*p;for(let E=0;E<y;E++){if(x.size>0&&!x.has(E))continue;const A=oe(E);for(let N=0;N<m;N++){const se=M+N*v+v*.1+E*T;if(e.type==="bar"){const U=e.values[E][N];if(!Number.isFinite(U))continue;const j=k+(1-u.frac(U))*p;a.fillStyle=A,a.globalAlpha=.9,a.fillRect(se+1,Math.min(j,le),Math.max(1,T-2),Math.max(1,Math.abs(le-j))),a.globalAlpha=1}else{const U=e.cells[E][N];if(U.length===0)continue;const j=se+T/2,Q=Math.max(3,Math.min(T*.7,46)),B=Be(Float64Array.from(U)),F=be=>k+(1-u.frac(be))*p;if(a.strokeStyle=A,a.fillStyle=A,a.lineWidth=1.5,U.length===1){a.beginPath(),a.arc(j,F(B.median),3,0,Math.PI*2),a.fill();continue}a.beginPath(),a.moveTo(j,F(B.min)),a.lineTo(j,F(B.max)),a.stroke();for(const be of[B.min,B.max])a.beginPath(),a.moveTo(j-Q/4,F(be)),a.lineTo(j+Q/4,F(be)),a.stroke();a.globalAlpha=.35,a.fillRect(j-Q/2,Math.min(F(B.q1),F(B.q3)),Q,Math.max(1,Math.abs(F(B.q1)-F(B.q3)))),a.globalAlpha=1,a.strokeRect(j-Q/2,Math.min(F(B.q1),F(B.q3)),Q,Math.max(1,Math.abs(F(B.q1)-F(B.q3)))),a.lineWidth=2.5,a.beginPath(),a.moveTo(j-Q/2,F(B.median)),a.lineTo(j+Q/2,F(B.median)),a.stroke()}}}if(_){const E=M+_.c*v+v*.1+_.g*T;a.lineWidth=1.5,a.strokeStyle=l.text,a.strokeRect(E,k,T,p)}}function Ae(e){if(!G||!g)return null;const l=g.getBoundingClientRect(),c=e.clientX-l.left-M;if(c<0||c>G.plotW)return null;const r=Math.min(G.n-1,Math.floor(c/G.slotW)),i=Math.floor((c-r*G.slotW-G.slotW*.1)/G.gw);return i<0||i>=G.groups?null:{g:i,c:r}}function De(e){const l=s,c=l.marks,r=l.level==="die"?"dies":"wafers",i=c.cells[e.g][e.c],o=`<strong>${I(c.categories[e.c])}${l.groups.length>1?` \xB7 ${I(ge(e.g))}`:""}</strong><br>`,t=l.y?.unit;if(i.length===0)return`${o}no ${r}`;const n=`${i.length.toLocaleString("en-GB")} ${r}`,f=c.categoryItems&&b.onOpenWafer?`<br><em>click to ${I(b.openActionLabel??"open this wafer")}</em>`:"";if(c.type==="bar"){const d=c.values[e.g][e.c];return`${o}${I(`${l.y.label}: ${R(d,t)}`)}<br>${I(n)}${f}`}const p=Be(Float64Array.from(i));return`${o}${["max","q3","median","q1","min"].map(d=>I(`${{max:"max",q3:"upper quartile",median:"median",q1:"lower quartile",min:"min"}[d]}: ${R(p[d],t)}`)).join("<br>")}<br>${I(n)}${f}`}function Ve(){const e=s?.marks;if(!s||!g||!s.x||!s.y||!e||e.type!=="line")return;ke(g,C??0);const l=ve($),{w:c,h:r,plotW:i,plotH:o}=pe(),t=Me(g,$,c,r);if(!t)return;const{ctx:n}=t,f=ie(s.x),p=ie(s.y),d=te(ae(e.xs,f),s.x),a=te(ae(e.values.flat().filter(Number.isFinite),p),s.y);z={x:d,y:a,plotW:i,plotH:o},ye(n,l,c,r,i,o,d,a,s.x,s.y),re(n,l.limitLine,i,o,d,f,s.x,"x"),re(n,l.limitLine,i,o,a,p,s.y,"y"),J=e.xs.map(h=>M+d.frac(h)*i),e.values.forEach((h,S)=>{if(x.size>0&&!x.has(S))return;n.strokeStyle=oe(S),n.fillStyle=oe(S),n.lineWidth=2;let u=!1;n.beginPath(),h.forEach((m,y)=>{if(!Number.isFinite(m)){u=!1;return}const v=J[y],T=k+(1-a.frac(m))*o;u?n.lineTo(v,T):n.moveTo(v,T),u=!0}),n.stroke(),h.forEach((m,y)=>{Number.isFinite(m)&&(n.beginPath(),n.arc(J[y],k+(1-a.frac(m))*o,D===y?4.5:3,0,Math.PI*2),n.fill())})}),D>=0&&(n.lineWidth=1,n.strokeStyle=l.textMuted,n.beginPath(),n.moveTo(J[D],k),n.lineTo(J[D],k+o),n.stroke())}function Ie(e){const l=s?.marks;if(!l||l.type!=="line"||!g||!s?.x||J.length===0)return null;const c=e.clientX-g.getBoundingClientRect().left;let r=-1,i=24;if(J.forEach((n,f)=>{const p=Math.abs(n-c);p<i&&(i=p,r=f)}),D=r,r<0)return null;const o=s.level==="die"?"dies":"wafers",t=l.values.map((n,f)=>({g:f,v:n[r],n:l.counts[f][r]})).filter(n=>Number.isFinite(n.v)&&(x.size===0||x.has(n.g)));return`<strong>${I(`${s.x.label}: ${R(l.xs[r],s.x.unit)}`)}</strong><br>`+t.map(n=>I(`${s.groups.length>1?`${ge(n.g)}: `:""}${R(n.v,s.y.unit)} (${n.n.toLocaleString("en-GB")} ${o})`)).join("<br>")}function W(){const e=s?.marks?.type;e==="scatter"?je():e==="histogram"?Ye():e==="box"||e==="bar"?Ge():e==="line"&&Ve()}function Ze(e){const l=s,c=b.waferLabel(e.item),r=e.die&&e.die.x!==void 0?`die (${e.die.x}, ${e.die.y})`:"",i=[c,r].filter(Boolean).join(" \xB7 "),o=(t,n)=>`${t.label}: ${R(n,t.unit)}`;return(i?`<strong>${I(i)}</strong><br>`:"")+I(o(l.x,e.x))+"<br>"+I(o(l.y,e.y))+(l.colorScale&&e.value!==void 0?`<br>${I(`${l.colorScale.label}: ${R(e.value,l.colorScale.unit)}`)}`:"")+(l.groups.length>1?`<br>${I(ge(e.group))}`:"")+(b.onOpenWafer?`<br><em>click to ${I(b.openActionLabel??"open this wafer")}</em>`:"")}function Ne(e){if(!K||!z||!s?.x)return null;const l=g.getBoundingClientRect(),r=(e.clientX-l.left-M)/z.plotW;if(r<0||r>1)return null;const i=z.x.reverse?1-r:r,{edges:o,counts:t}=K,n=o.length-1,f=Math.min(n-1,Math.max(0,Math.floor(i*n)));X=f;const p=s.x,d=t.map((a,h)=>({g:h,n:a[f]})).filter(a=>a.n>0&&(x.size===0||x.has(a.g)));return`<strong>${I(`${R(o[f],p.unit)} to ${R(o[f+1],p.unit)}`)}</strong><br>`+(d.length?d.map(a=>I(`${s.groups.length>1?`${ge(a.g)}: `:""}${a.n.toLocaleString("en-GB")} ${s.level==="die"?"dies":"wafers"}`)).join("<br>"):"none")}function Ke(e){const l=P.createElement("div");l.dataset.wmapPlotColorbar="1",Object.assign(l.style,{display:"flex",alignItems:"center",gap:ee.md,marginTop:ee.md,marginBottom:ee.xl,fontSize:we.body,color:xe.label});const c=P.createElement("canvas");c.width=200,c.height=10,Object.assign(c.style,{width:"200px",height:"10px",borderRadius:tt.control,flex:"0 0 auto"});const r=c.getContext("2d"),i=$e();if(r)for(let f=0;f<200;f++)r.fillStyle=i(f/199),r.fillRect(f,0,1,10);const o=P.createElement("span");o.textContent=R(e.lo,e.unit);const t=P.createElement("span");t.textContent=R(e.hi,e.unit);const n=P.createElement("span");return n.textContent=e.label,n.style.color=xe.text,l.append(n,o,c,t),l}const Se=(e,l,c)=>l.map(r=>{const{item:i,die:o}=e(r);return{x:0,y:0,group:c,item:i,die:o}});function Je(){V?.destroy(),V=void 0,w.innerHTML="",g=void 0,H=void 0,C=void 0,Z=[],ue=null,X=-1,ce=[],z=void 0,G=void 0,_=null,J=[],D=-1,de=he,x.clear(),Y.clear();const e=s;if(!e)return;if(q.textContent=et(e),$.dataset.wmapChartTitle=q.textContent,e.issues.length||!e.marks){for(const i of e.issues)Re(w,i);return}const l=P.createElement("div");Object.assign(l.style,{color:xe.label,fontSize:we.body,marginBottom:ee.xs});const c=b.openActionLabel??"open this wafer",r=e.marks.type==="scatter"?(b.onOpenWafer?` \xB7 click a point to ${c}`:"")+(b.onSelectPoints?` \xB7 drag to select ${e.level==="die"?"dies":"wafers"}`:""):e.marks.type==="box"||e.marks.type==="bar"?(b.onOpenWafer&&e.marks.categoryItems?` \xB7 click to ${c}`:b.onSelectPoints?" \xB7 click to chart or tabulate what it counts":"")+(e.marks.type==="box"?" \xB7 box: quartiles; whiskers: minimum and maximum":""):e.marks.type==="histogram"&&b.onSelectPoints?" \xB7 click a bar to chart or tabulate its dies":e.marks.type==="line"&&b.onSelectPoints?" \xB7 click a point to chart or tabulate what it averages":"";l.textContent=Fe(e)+r,ze(l),C=P.createElement("div"),C.style.display="flow-root",w.appendChild(C),C.appendChild(l);for(const i of e.notes){const o=P.createElement("div");ze(o),o.textContent=i,Object.assign(o.style,{color:xe.warnText,fontSize:we.body,marginBottom:ee.xs}),C.appendChild(o)}if(e.plotted===0){Re(w,"Nothing to plot: no mark has every value this plot needs.");return}if(e.groups.length>1&&(H=P.createElement("div"),Object.assign(H.style,{display:"flex",flexWrap:"wrap",gap:ee.sm,marginTop:ee.md,marginBottom:ee.xl}),e.groups.forEach((i,o)=>{const t=ft(i||"(none)",oe(o),P);lt(t.el,$,L,`${i||"(none)"} \u2014 click to filter`),t.el.addEventListener("click",()=>{x.has(o)?x.delete(o):x.add(o),Z.forEach((n,f)=>n.setState({selected:x.has(f),dimmed:x.size>0&&!x.has(f)})),W()}),Z.push(t),H.appendChild(t.el)}),C.appendChild(H)),e.colorScale&&(H=Ke(e.colorScale),C.appendChild(H)),g=P.createElement("canvas"),g.style.display="block",g.style.cursor=e.marks.type==="scatter"?"crosshair":"default",w.appendChild(g),e.marks.type==="scatter"){const i=e.marks,o=!!b.onSelectPoints;V=yt({card:$,canvas:g,tooltip:L,drawn:()=>ce,describe:Ze,clickable:()=>!!b.onOpenWafer,onOpen:t=>b.onOpenWafer?.(t.item,Oe(e.spec)),onHover:t=>{ue=t,W()},select:o?{pick:({x0:t,y0:n,x1:f,y1:p})=>z?(x.size===0?i.points:i.points.filter(a=>x.has(a.group))).filter(a=>{const h=M+z.x.frac(a.x)*z.plotW,S=k+(1-z.y.frac(a.y))*z.plotH;return h>=t&&h<=f&&S>=n&&S<=p}):[],onPicked:(t,n)=>{Y.clear();for(const f of t)Y.add(f);W(),t.length>0&&b.onSelectPoints(t,n,g)},onClear:()=>{Y.clear(),W()},hasSelection:()=>Y.size>0}:void 0})}else if(e.marks.type==="histogram"){Ue(),g.addEventListener("mousemove",o=>{const t=Ne(o);if(!t){L.style.display="none",X>=0&&(X=-1,W());return}L.innerHTML=t,L.style.display="block",Le(L,$,o.clientX,o.clientY),W()}),g.addEventListener("mouseleave",()=>{L.style.display="none",X>=0&&(X=-1,W())});const i=e.marks;b.onSelectPoints&&(g.style.cursor="pointer",g.addEventListener("click",o=>{if(!K||Ne(o)===null)return;const t=X,{edges:n}=K,f=z?.x.log,p=f?Math.log10(n[0]):n[0],d=f?Math.log10(n[n.length-1]):n[n.length-1],a=n.length-1,h=[];i.values.forEach((S,u)=>{if(x.size>0&&!x.has(u))return;const m=[];S.forEach((y,v)=>{const T=f?y>0?Math.log10(y):NaN:y;!Number.isFinite(T)||T<p||T>d||Math.min(a-1,Math.floor((T-p)/(d-p)*a))===t&&m.push(i.units[u][v])}),h.push(...Se(i.unit,m,u))}),h.length>0&&(L.style.display="none",b.onSelectPoints(h,{x:o.clientX,y:o.clientY},g))}))}else if(e.marks.type==="box"||e.marks.type==="bar"){const i=e.marks;g.addEventListener("mousemove",o=>{const t=Ae(o);if(g.style.cursor=t&&(i.categoryItems&&b.onOpenWafer||b.onSelectPoints&&i.cellUnits[t.g][t.c].length>0)?"pointer":"default",!t){L.style.display="none",_&&(_=null,W());return}L.innerHTML=De(t),L.style.display="block",Le(L,$,o.clientX,o.clientY),(!_||_.g!==t.g||_.c!==t.c)&&(_=t,W())}),g.addEventListener("mouseleave",()=>{L.style.display="none",_&&(_=null,W())}),g.addEventListener("click",o=>{const t=Ae(o);if(!t)return;const n=i.categoryItems?.[t.c];if(n!==void 0&&n>=0&&b.onOpenWafer){b.onOpenWafer(n,Oe(e.spec));return}const f=i.cellUnits[t.g][t.c];f.length>0&&b.onSelectPoints&&(L.style.display="none",b.onSelectPoints(Se(i.unit,f,t.g),{x:o.clientX,y:o.clientY},g))})}else if(e.marks.type==="line"){g.addEventListener("mousemove",o=>{const t=Ie(o);if(!t){L.style.display="none",W();return}L.innerHTML=t,L.style.display="block",Le(L,$,o.clientX,o.clientY),W()}),g.addEventListener("mouseleave",()=>{L.style.display="none",D>=0&&(D=-1,W())});const i=e.marks;b.onSelectPoints&&(g.style.cursor="pointer",g.addEventListener("click",o=>{if(Ie(o)===null||D<0)return;const t=i.cellUnits.flatMap((n,f)=>x.size>0&&!x.has(f)?[]:Se(i.unit,n[D]??[],f));t.length>0&&(L.style.display="none",b.onSelectPoints(t,{x:o.clientX,y:o.clientY},g))}))}W()}ne(()=>{const e=s;if(!e||!e.marks)return;const l=$e();return{caption:[Fe(e),...e.notes],legend:e.groups.length>1?e.groups.map((c,r)=>({label:c||"(none)",color:oe(r)})):void 0,colorbar:e.colorScale?{label:e.colorScale.label,lo:R(e.colorScale.lo,e.colorScale.unit),hi:R(e.colorScale.hi,e.colorScale.unit),color:l}:void 0}});const Qe=ot($,()=>W());return{card:$,actions:O,setPlot(e){s=e,Je()},destroy(){Qe.disconnect(),V?.destroy()}}}
@@ -0,0 +1,45 @@
1
+ /** A point as drawn, with its pixel position inside the canvas. */
2
+ export interface DrawnPoint<P> {
3
+ p: P;
4
+ cx: number;
5
+ cy: number;
6
+ }
7
+ export interface PointInteractionOptions<P> {
8
+ card: HTMLElement;
9
+ canvas: HTMLCanvasElement;
10
+ tooltip: HTMLElement;
11
+ /** The points as last drawn. Only these can be hit: a chart that samples a large population hides the rest. */
12
+ drawn: () => ReadonlyArray<DrawnPoint<P>>;
13
+ /** Finds the point a pointer is on by its own rule, in place of the nearest drawn point within a few pixels: a chart
14
+ * whose points are columns (one per wafer) is hit anywhere along the column, not only on the dot. */
15
+ locate?: (e: MouseEvent) => P | null;
16
+ /** Tooltip HTML for a point (already escaped). */
17
+ describe: (p: P) => string;
18
+ /** Whether a click on this point does anything. */
19
+ clickable: (p: P) => boolean;
20
+ onOpen?: (p: P) => void;
21
+ /** The point under the pointer changed (null when it left all points): redraw. */
22
+ onHover: (p: P | null) => void;
23
+ /** Present to enable drag-to-select. */
24
+ select?: {
25
+ /** The points inside a rectangle given in canvas pixels. */
26
+ pick: (rect: {
27
+ x0: number;
28
+ y0: number;
29
+ x1: number;
30
+ y1: number;
31
+ }) => P[];
32
+ /** A drag finished: the chart records the selection, redraws and tells its host. `at` is the pointer in the viewport. */
33
+ onPicked: (picked: P[], at: {
34
+ x: number;
35
+ y: number;
36
+ }) => void;
37
+ /** A click on empty space while a selection stands. */
38
+ onClear: () => void;
39
+ hasSelection: () => boolean;
40
+ };
41
+ }
42
+ export declare function wirePointInteractions<P>(o: PointInteractionOptions<P>): {
43
+ destroy: () => void;
44
+ };
45
+ //# sourceMappingURL=pointInteractions.d.ts.map
@@ -0,0 +1 @@
1
+ import{CLR as E}from"../toolbar.js";import{positionChartTooltip as L}from"./chartShell.js";const M=7,k=4;export function wirePointInteractions(n){const{card:u,canvas:r,tooltip:a}=n;let d=null;const x=t=>{if(n.locate)return n.locate(t);const e=r.getBoundingClientRect(),s=t.clientX-e.left,l=t.clientY-e.top;let p=null,i=M*M;for(const o of n.drawn()){const c=o.cx-s,h=o.cy-l,y=c*c+h*h;y<=i&&(i=y,p=o.p)}return p};r.addEventListener("mousemove",t=>{const e=x(t);if(r.style.cursor=e&&n.clickable(e)?"pointer":"crosshair",!e){a.style.display="none",d&&(d=null,n.onHover(null));return}a.innerHTML=n.describe(e),a.style.display="block",L(a,u,t.clientX,t.clientY),d!==e&&(d=e,n.onHover(e))}),r.addEventListener("mouseleave",()=>{a.style.display="none",d&&(d=null,n.onHover(null))});let b=!1;const m=u.ownerDocument.createElement("div");Object.assign(m.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"40",boxSizing:"border-box",border:`1px dashed ${E.iconActive}`,background:"rgba(120,150,200,0.15)"}),u.appendChild(m);const f=n.select;return f&&r.addEventListener("mousedown",t=>{if(t.button!==0)return;b=!1;const e=r.getBoundingClientRect(),s=t.clientX-e.left,l=t.clientY-e.top;let p=!1,i=s,o=l;const c=u.ownerDocument,h=v=>{if(i=Math.min(Math.max(v.clientX-e.left,0),e.width),o=Math.min(Math.max(v.clientY-e.top,0),e.height),!p&&Math.hypot(i-s,o-l)<k)return;p=!0,a.style.display="none";const g=u.getBoundingClientRect();Object.assign(m.style,{display:"block",left:`${e.left-g.left+Math.min(s,i)}px`,top:`${e.top-g.top+Math.min(l,o)}px`,width:`${Math.abs(i-s)}px`,height:`${Math.abs(o-l)}px`})},y=v=>{c.removeEventListener("mousemove",h),c.removeEventListener("mouseup",y),m.style.display="none",p&&(b=!0,f.onPicked(f.pick({x0:Math.min(s,i),y0:Math.min(l,o),x1:Math.max(s,i),y1:Math.max(l,o)}),{x:v.clientX,y:v.clientY}))};c.addEventListener("mousemove",h),c.addEventListener("mouseup",y)}),r.addEventListener("click",t=>{if(b){b=!1;return}const e=x(t);if(!e){f?.hasSelection()&&f.onClear();return}n.clickable(e)&&n.onOpen?.(e)}),{destroy:()=>m.remove()}}
@@ -6,6 +6,8 @@ export interface RegionYieldDiagramOptions {
6
6
  mode: RegionYieldMode;
7
7
  /** Ring mode: ordered ring 1 (core) → ring N (edge), matching `buildRegionYieldData`'s output for `buildRingRegions`. Quadrant mode: any order — each row's quadrant is read from its `key` (`quadrant:NE` etc.), not position. */
8
8
  rows: RegionYieldDatum[];
9
+ /** Click a region: called with it and the click, so the host can pick out the dies it counts and open a menu there. */
10
+ onSelectRegion?: (row: RegionYieldDatum, e: MouseEvent) => void;
9
11
  onSaveImage?: SaveImageHandler;
10
12
  /** Document to build this panel's DOM into. Default `document` — pass the
11
13
  * host's own `ownerDocument` when the container might live in a
@@ -1 +1 @@
1
- import{parseRegionKey as A}from"../../stats/regions.js";import{yieldFill as D}from"./palette.js";import{fontPx as C}from"../toolbar.js";import{cardShell as N,resolveChartCanvasColors as F,observeResize as $,growCardToFitContent as j,setChartGrow as q,isExpandedCard as z,bodyRoom as G,renderEmptyState as O,prepareCanvas as _}from"./chartShell.js";const v=320,L=220,Q={SE:[0,Math.PI/2],SW:[Math.PI/2,Math.PI],NW:[Math.PI,1.5*Math.PI],NE:[1.5*Math.PI,2*Math.PI]};function Y(e,a,o,r,l,n){e.beginPath(),e.moveTo(a+n,o),e.arcTo(a+r,o,a+r,o+l,n),e.arcTo(a+r,o+l,a,o+l,n),e.arcTo(a,o+l,a,o,n),e.arcTo(a,o,a+r,o,n),e.closePath()}function w(e,a,o,r,l,n){e.font=`${C(-1)}px system-ui, sans-serif`;const c=e.measureText(r).width;e.font="700 11px system-ui, sans-serif";const y=e.measureText(l).width,m=Math.max(c,y)+12,g=30;e.fillStyle=n.bg,Y(e,a-m/2,o-g/2,m,g,4),e.fill(),e.strokeStyle=n.border,e.lineWidth=1,e.stroke(),e.textAlign="center",e.textBaseline="middle",e.fillStyle=n.textMuted,e.font=`${C(-1)}px system-ui, sans-serif`,e.fillText(r,a,o-7),e.fillStyle=n.text,e.font="700 11px system-ui, sans-serif",e.fillText(l,a,o+7)}export function renderRegionYieldDiagram(e){const{mode:a,rows:o,onSaveImage:r}=e,l=e.title??(a==="ring"?"Ring yield":"Quadrant yield"),{card:n,body:c}=N(l,r,e.ownerDocument);q(n,"square");const y=o.length>0;if(!y)return O(c,"No bin data available for regional yield."),{card:n,hasData:y,destroy:()=>{}};Object.assign(c.style,{display:"flex",justifyContent:"center",alignItems:"center"});const m=n.ownerDocument.createElement("canvas");Object.assign(m.style,{display:"block",flex:"none"}),c.appendChild(m);function g(){const S=Math.max(L,Math.min(c.clientWidth||v,v));j(n,c,S);const b=z(n)?Math.max(S,Math.floor(Math.min(c.clientWidth,G(n,c,m)))):S,k=_(m,n,b,b);if(!k)return;const{ctx:t}=k,u=F(n),T=D,d=b/2,h=b/2,f=b/2-34;if(a==="ring"){const i=o.length;for(let s=0;s<i;s++){const M=s/i*f,P=(s+1)/i*f;t.beginPath(),t.arc(d,h,P,0,2*Math.PI),t.arc(d,h,M,0,2*Math.PI,!0),t.closePath(),t.fillStyle=T(o[s].yieldPercent),t.fill(),t.strokeStyle=u.border,t.lineWidth=1,t.stroke()}t.beginPath(),t.arc(d,h,f,0,2*Math.PI),t.strokeStyle=u.text,t.lineWidth=1.5,t.stroke();for(let s=0;s<i;s++){const M=(s+.5)/i*f,P=-Math.PI/2+s/i*2*Math.PI,I=d+Math.cos(P)*M,p=h+Math.sin(P)*M;w(t,I,p,o[s].label,`${o[s].yieldPercent.toFixed(1)}%`,u)}}else{for(const i of o){const s=A(i.key).quadrant,M=s?Q[s]:void 0;if(!M)continue;const[P,I]=M;t.beginPath(),t.moveTo(d,h),t.arc(d,h,f,P,I),t.closePath(),t.fillStyle=T(i.yieldPercent),t.fill(),t.strokeStyle=u.border,t.lineWidth=1,t.stroke();const p=(P+I)/2,W=d+Math.cos(p)*f*.6,E=h+Math.sin(p)*f*.6;w(t,W,E,i.label,`${i.yieldPercent.toFixed(1)}%`,u)}t.beginPath(),t.arc(d,h,f,0,2*Math.PI),t.strokeStyle=u.text,t.lineWidth=1.5,t.stroke()}}const R=$(n,()=>g());return g(),{card:n,hasData:y,destroy:()=>R.disconnect()}}
1
+ import{parseRegionKey as T}from"../../stats/regions.js";import{yieldFill as q}from"./palette.js";import{fontPx as v}from"../toolbar.js";import{cardShell as $,resolveChartCanvasColors as j,observeResize as z,growCardToFitContent as G,setChartGrow as L,isExpandedCard as O,bodyRoom as Y,renderEmptyState as _,prepareCanvas as B}from"./chartShell.js";const w=320,Q=220,E={SE:[0,Math.PI/2],SW:[Math.PI/2,Math.PI],NW:[Math.PI,1.5*Math.PI],NE:[1.5*Math.PI,2*Math.PI]};function X(e,a,n,h,f,o){e.beginPath(),e.moveTo(a+o,n),e.arcTo(a+h,n,a+h,n+f,o),e.arcTo(a+h,n+f,a,n+f,o),e.arcTo(a,n+f,a,n,o),e.arcTo(a,n,a+h,n,o),e.closePath()}function W(e,a,n,h,f,o){e.font=`${v(-1)}px system-ui, sans-serif`;const u=e.measureText(h).width;e.font="700 11px system-ui, sans-serif";const p=e.measureText(f).width,c=Math.max(u,p)+12,y=30;e.fillStyle=o.bg,X(e,a-c/2,n-y/2,c,y,4),e.fill(),e.strokeStyle=o.border,e.lineWidth=1,e.stroke(),e.textAlign="center",e.textBaseline="middle",e.fillStyle=o.textMuted,e.font=`${v(-1)}px system-ui, sans-serif`,e.fillText(h,a,n-7),e.fillStyle=o.text,e.font="700 11px system-ui, sans-serif",e.fillText(f,a,n+7)}export function renderRegionYieldDiagram(e){const{mode:a,rows:n,onSaveImage:h}=e,f=e.title??(a==="ring"?"Ring yield":"Quadrant yield"),{card:o,body:u}=$(f,h,e.ownerDocument);L(o,"square");const p=n.length>0;if(!p)return _(u,"No bin data available for regional yield."),{card:o,hasData:p,destroy:()=>{}};Object.assign(u.style,{display:"flex",justifyContent:"center",alignItems:"center"});const c=o.ownerDocument.createElement("canvas");Object.assign(c.style,{display:"block",flex:"none"}),u.appendChild(c);let y={cx:0,cy:0,R:0};function C(){const g=Math.max(Q,Math.min(u.clientWidth||w,w));G(o,u,g);const d=O(o)?Math.max(g,Math.floor(Math.min(u.clientWidth,Y(o,u,c)))):g,P=B(c,o,d,d);if(!P)return;const{ctx:t}=P,m=j(o),S=q,s=d/2,i=d/2,M=d/2-34;if(y={cx:s,cy:i,R:M},a==="ring"){const l=n.length;for(let r=0;r<l;r++){const b=r/l*M,I=(r+1)/l*M;t.beginPath(),t.arc(s,i,I,0,2*Math.PI),t.arc(s,i,b,0,2*Math.PI,!0),t.closePath(),t.fillStyle=S(n[r].yieldPercent),t.fill(),t.strokeStyle=m.border,t.lineWidth=1,t.stroke()}t.beginPath(),t.arc(s,i,M,0,2*Math.PI),t.strokeStyle=m.text,t.lineWidth=1.5,t.stroke();for(let r=0;r<l;r++){const b=(r+.5)/l*M,I=-Math.PI/2+r/l*2*Math.PI,R=s+Math.cos(I)*b,k=i+Math.sin(I)*b;W(t,R,k,n[r].label,`${n[r].yieldPercent.toFixed(1)}%`,m)}}else{for(const l of n){const r=T(l.key).quadrant,b=r?E[r]:void 0;if(!b)continue;const[I,R]=b;t.beginPath(),t.moveTo(s,i),t.arc(s,i,M,I,R),t.closePath(),t.fillStyle=S(l.yieldPercent),t.fill(),t.strokeStyle=m.border,t.lineWidth=1,t.stroke();const k=(I+R)/2,N=s+Math.cos(k)*M*.6,F=i+Math.sin(k)*M*.6;W(t,N,F,l.label,`${l.yieldPercent.toFixed(1)}%`,m)}t.beginPath(),t.arc(s,i,M,0,2*Math.PI),t.strokeStyle=m.text,t.lineWidth=1.5,t.stroke()}}function A(g,d){const P=g-y.cx,t=d-y.cy,m=Math.hypot(P,t);if(!(m<=y.R))return null;if(a==="ring")return n[Math.min(n.length-1,Math.floor(m/y.R*n.length))]??null;const S=(Math.atan2(t,P)+2*Math.PI)%(2*Math.PI);return n.find(s=>{const i=E[T(s.key).quadrant??""];return!!i&&S>=i[0]&&S<i[1]})??null}e.onSelectRegion&&(c.style.cursor="pointer",c.addEventListener("click",g=>{const d=c.getBoundingClientRect(),P=A(g.clientX-d.left,g.clientY-d.top);P&&e.onSelectRegion(P,g)}));const D=z(o,()=>C());return C(),{card:o,hasData:p,destroy:()=>D.disconnect()}}
@@ -42,6 +42,25 @@ export interface ScatterPanelOptions {
42
42
  axisPrefs?: AxisPrefs;
43
43
  /** Called when the user changes the "Limits:" choice here. */
44
44
  onAxisPrefsChange?: (prefs: AxisPrefs) => void;
45
+ /**
46
+ * Click a point to open its die's wafer, on the X test — the same
47
+ * `(waferIndex, testNumber)` the boxplot and trend use. Needs each item's
48
+ * `waferIndex`; a point without one is not clickable. Omit for no click action.
49
+ */
50
+ onOpen?: (waferIndex: number, testNumber: number) => void;
51
+ /** What a click does, in the reader's words, when it is not "open this wafer" (a single-wafer host
52
+ * shows the X test on its map instead). Default `open this wafer`. */
53
+ openActionLabel?: string;
54
+ /**
55
+ * Drag a rectangle over the plot to select the dies inside it (only points the
56
+ * legend filter leaves visible). Called on release with those points, the
57
+ * pointer position and the canvas, so the host can open a menu there. The
58
+ * selection stays ringed until X/Y change or a click on empty space.
59
+ */
60
+ onSelect?: (points: ScatterPoint[], at: {
61
+ x: number;
62
+ y: number;
63
+ }, anchor: HTMLElement, xTestNumber: number) => void;
45
64
  /** Document to build this panel's DOM into. Default `document` — pass the
46
65
  * host's own `ownerDocument` when the container might live in a
47
66
  * different document (e.g. a gallery card detached into its own popup
@@ -1 +1 @@
1
- import{NO_DATA_FILL as Ce}from"../../renderer/colorMap.js";import{maxOf as J,minOf as Le}from"../../core/utils.js";import{categorical as Xe}from"./palette.js";import{buildScatterData as Ye,buildScatterDataGrouped as Ke}from"../../stats/scatter.js";import{pearsonOfPairs as Ve}from"../../stats/correlation.js";import{buildFacetTable as qe}from"../../stats/facets.js";import{SPACE as k,RADIUS as Je,fontPx as L,FONT as re,CLR as X}from"../toolbar.js";import{fitTicks as Ae}from"../../renderer/axisTicks.js";import{cardShell as Qe,observeResize as Ze,makeTooltip as et,attachChartTip as tt,makeTestSelect as nt,makeWaferSelect as it,chartFillHeight as ot,applyCanvasFlow as st,drawAxisUnit as lt,resolveChartCanvasColors as at,makeAxisFormat as Q,horizontalTickSpacing as rt,VERTICAL_TICK_SPACING_PX as ct,makeSeriesLegendItem as ft,prepareCanvas as dt,drawOffAxisLimits as Oe,limitLines as mt,hasBothLimitKinds as ut,makeLimitsSelect as pt,stackLabelRows as Pe,strokeLimitLine as De,fillTextOnHalo as Be}from"./chartShell.js";const c=52,xt=16,l=16,ht=44;export const NO_BIN_CATEGORY="none";export function binCategoryOf(h){return h===void 0?NO_BIN_CATEGORY:String(h)}export function binCategories(h){const Y=new Set;let S=!1;for(const O of h)O.hbin===void 0?S=!0:Y.add(O.hbin);const A=Array.from(Y).sort((O,Z)=>O-Z).map(String);return S&&A.push(NO_BIN_CATEGORY),A}export function renderScatterPanel(h){const{title:Y="Test scatter",items:S,testDefs:A,binColors:O,onSaveImage:Z,groups:K}=h,{card:s,body:P,controlsRow:ee}=Qe(Y,Z,h.ownerDocument),V=A.filter(e=>e.testNumber!==void 0),v=!!(K&&K.length>0),ce=(K??[]).map(e=>e.key),Ee=new Map(ce.map((e,n)=>[e,n]));let p=h.xTestNumber??V[0]?.testNumber??null,b=h.yTestNumber??V[1]?.testNumber??p,R=null;const fe=()=>R!==null?S[R]?[S[R]]:[]:S;function de(e,n,o){const r=s.ownerDocument.createElement("label");Object.assign(r.style,{display:"inline-flex",alignItems:"center",gap:k.xs,fontSize:re.body,color:X.label});const T=s.ownerDocument.createElement("span");T.textContent=e;const d=nt(V,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:s.ownerDocument});return r.append(T,d),{wrap:r,select:d}}const{wrap:$e,select:Re}=de("X:",p,e=>{p=e,F()}),{wrap:Ie,select:We}=de("Y:",b,e=>{b=e,F()});ee.append($e,Ie);let M=h.axisPrefs??{clipOutliers:!1};const me=s.ownerDocument.createElement("span");ee.appendChild(me);function ue(){const e=[p,b].map(n=>A.find(o=>o.testNumber===n));me.replaceChildren(...e.some(ut)?[pt(M.limits??"both",n=>{M={...M,limits:n},h.onAxisPrefsChange?.(M),E()},s.ownerDocument)]:[])}!v&&S.length>1&&ee.appendChild(it(S,R,e=>{R=e,F()},{ownerDocument:s.ownerDocument}));const I=s.ownerDocument.createElement("div");Object.assign(I.style,{color:X.warnText,background:X.warnBg,border:`1px solid ${X.warnBorder}`,borderRadius:Je.control,padding:`${k.xs} ${k.md}`,fontSize:re.body,marginBottom:k.xs,display:"none"}),s.insertBefore(I,P);function pe(){const e=v?[]:qe(fe(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);e.length>0?(I.textContent=`\u26A0 Mixed ${e.join(", ")} across these points \u2014 trends here may be confounded (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,I.style.display=""):I.style.display="none"}pe();const te=s.ownerDocument.createElement("div");Object.assign(te.style,{color:X.label,fontSize:re.body,marginBottom:k.xs}),s.insertBefore(te,P);function ne(){const e=v?"One point per die \xB7 coloured by group \xB7 click legend to filter":"One point per die across all wafers \xB7 coloured by hard bin \xB7 click legend to filter",n=g.size===0?x:x.filter(T=>g.has(q(T))),{r:o,n:r}=Ve(n);te.textContent=o===null?`${e} \xB7 n = ${r.toLocaleString()}`:`${e} \xB7 r = ${o.toFixed(3)} \xB7 n = ${r.toLocaleString()}`}const q=e=>v?e.group??"\u2014":binCategoryOf(e.hbin),xe=e=>v?Xe(Ee.get(e)??0):e===NO_BIN_CATEGORY?Ce:O?.get(Number(e))??Ce,he=e=>v?e:e===NO_BIN_CATEGORY?"No bin data":`Bin ${e}`,g=new Set,W=s.ownerDocument.createElement("div");Object.assign(W.style,{display:"flex",flexWrap:"wrap",gap:k.sm,marginTop:k.md,marginBottom:k.xl}),P.appendChild(W);const D=s.ownerDocument.createElement("canvas");D.style.display="block",D.style.cursor="crosshair",P.appendChild(D);const Ne=et(s);let x=[],y=0,C=1,w=0,B=1;function N(e){const n=A.find(o=>o.testNumber===e);return{unit:n?.unit,lines:mt(n,M.limits)}}function Fe(){const e=P.clientWidth,n=Math.max(200,Math.min(400,e*.65)),o=ot(s,P,D,n);return{w:e,h:o,plotW:Math.max(10,e-c-xt),plotH:Math.max(10,o-l-ht)}}const ie=new Map;function be(){for(const[e,n]of ie){const o=g.has(e);n.setState({selected:o,dimmed:g.size>0&&!o})}}function ge(e){W.innerHTML="",ie.clear(),g.clear();for(const n of e){const o=ft(he(n),xe(n),s.ownerDocument),r=o.el;r.dataset.cat=n,ie.set(n,o),tt(r,s,Ne,`${he(n)} \u2014 click to filter`),r.addEventListener("click",()=>{g.has(n)?g.delete(n):g.add(n),be(),ne(),E()}),W.appendChild(r)}be()}function E(){st(D,W);const e=at(s),n=C-y,o=B-w,{w:r,h:T,plotW:d,plotH:f}=Fe(),H=dt(D,s,r,T);if(!H)return;const{ctx:t}=H;if(x.length===0){t.font=`${L()}px system-ui, sans-serif`,t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="middle",t.fillText("No data \u2014 select two parametric tests with values.",r/2,T/2);return}t.font=`${L(-1)}px system-ui, sans-serif`;const _=p!==null?N(p).unit:void 0,ye=Ae(y,C,d,rt(t,Math.max(Math.abs(y),Math.abs(C)),_)),Te=Ae(w,B,f,()=>ct),oe=Q(Math.max(Math.abs(y),Math.abs(C)),p!==null?N(p).unit:void 0,ye.step||void 0),se=Q(Math.max(Math.abs(w),Math.abs(B)),b!==null?N(b).unit:void 0,Te.step||void 0);t.font=`${L(-1)}px system-ui, sans-serif`,t.strokeStyle=e.grid,t.lineWidth=.5,t.fillStyle=e.textMuted;for(const m of ye.ticks){const u=c+(m-y)/n*d;t.beginPath(),t.moveTo(u,l),t.lineTo(u,l+f),t.stroke(),t.textAlign="center",t.textBaseline="top",t.fillText(oe.tick(m),u,l+f+4)}for(const m of Te.ticks){const u=l+(1-(m-w)/o)*f;t.beginPath(),t.moveTo(c,u),t.lineTo(c+d,u),t.stroke(),t.textAlign="right",t.textBaseline="middle",t.fillText(se.tick(m),c-4,u)}oe.unitLabel&&lt(t,oe.unitLabel,c+d/2,l+f+24,e.textMuted),se.unitLabel&&(t.save(),t.font=`${L(-1)}px system-ui, sans-serif`,t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="middle",t.translate(6,l+f/2),t.rotate(-Math.PI/2),t.fillText(`(${se.unitLabel})`,0,0),t.restore());const z=g.size===0?x:x.filter(m=>g.has(q(m))),we=z.length>5e3?Math.ceil(z.length/5e3):1;t.globalAlpha=Math.max(.15,Math.min(.7,200/(z.length/we)));for(let m=0;m<z.length;m+=we){const u=z[m],G=c+(u.x-y)/n*d,j=l+(1-(u.y-w)/o)*f;t.beginPath(),t.arc(G,j,2.5,0,Math.PI*2),t.fillStyle=xe(q(u)),t.fill()}if(t.globalAlpha=1,p!==null&&b!==null){const m=N(p),u=N(b);t.strokeStyle=e.limitLine,t.fillStyle=e.limitLine,t.lineWidth=1,t.font=`${L(-1)}px system-ui, sans-serif`,t.textBaseline="top";const G=L(-1)+2,j=m.lines.filter(i=>i.value>=y&&i.value<=C).map(i=>{const a=c+(i.value-y)/n*d;return{l:i,cx:a,start:a+2,end:a+2+t.measureText(i.label).width}}),Se=Pe(j);j.forEach(({l:i,cx:a},U)=>{De(t,i,e.limitLine,a,l,a,l+f),t.textAlign="left",Be(t,i.label,a+2,l+2+Se[U]*G,e.bg)});const le=u.lines.filter(i=>i.value>=w&&i.value<=B).map(i=>{const a=l+(1-(i.value-w)/o)*f;return{l:i,cy:a,start:a+2,end:a+2+L(-1)}}),Ge=Pe(le,1),je=J(le.map(i=>t.measureText(i.l.label).width))+8;le.forEach(({l:i,cy:a},U)=>{De(t,i,e.limitLine,c,a,c+d,a),t.textAlign="left",Be(t,i.label,c+3+Ge[U]*je,a+2,e.bg)});const ve=(i,a,U)=>i.filter($=>$.value<a||$.value>U).map($=>({value:$.value,label:$.label,side:$.value<a?"lo":"hi"})),Me={left:c,right:c+d,top:l,bottom:l+f},ae=ve(m.lines,y,C),ke=l+(j.length?(J(Se)+1)*G:0),Ue=Math.max(ae.filter(i=>i.side==="lo").length,ae.filter(i=>i.side==="hi").length);Oe(t,ae,{...Me,top:ke},"horizontal",e.limitLine,i=>Q(Math.abs(i),m.unit).tick(i),e.bg),Oe(t,ve(u.lines,w,B),{...Me,top:ke+Ue*G},"vertical",e.limitLine,i=>Q(Math.abs(i),u.unit).tick(i),e.bg),t.setLineDash([])}t.strokeStyle=e.border,t.lineWidth=1,t.beginPath(),t.moveTo(c,l),t.lineTo(c,l+f),t.moveTo(c,l+f),t.lineTo(c+d,l+f),t.stroke()}function F(){if(pe(),ue(),V.length<2||p===null||b===null){x=[],ge([]),ne(),E();return}x=v?Ke(K,p,b):Ye(fe(),p,b);let e;if(v){const n=new Set(x.map(q));e=ce.filter(o=>n.has(o))}else e=binCategories(x);if(ge(e),ne(),x.length>0){const n=x.map(_=>_.x),o=x.map(_=>_.y),r=Le(n),T=J(n),d=Le(o),f=J(o),H=(T-r)*.05||1,t=(f-d)*.05||1;y=r-H,C=T+H,w=d-t,B=f+t}E()}const He=Ze(s,()=>E());F();function _e(e,n){p=e,b=n,Re.value=String(e),We.value=String(n),F()}function ze(e){if((e.limits??"both")===(M.limits??"both")){M=e;return}M=e,ue(),E()}return{card:s,setXY:_e,setAxisPrefs:ze,destroy:()=>He.disconnect()}}
1
+ import{NO_DATA_FILL as Ne}from"../../renderer/colorMap.js";import{escHtml as Z,maxOf as se,minOf as Re}from"../../core/utils.js";import{fmt as He}from"../../renderer/fmt.js";import{categorical as st}from"./palette.js";import{buildScatterData as lt,buildScatterDataGrouped as at}from"../../stats/scatter.js";import{pearsonOfPairs as rt}from"../../stats/correlation.js";import{buildFacetTable as ct}from"../../stats/facets.js";import{SPACE as D,RADIUS as dt,fontPx as z,FONT as ge,CLR as ee}from"../toolbar.js";import{fitTicks as Fe}from"../../renderer/axisTicks.js";import{wirePointInteractions as ft}from"./pointInteractions.js";import{cardShell as mt,observeResize as ut,makeTooltip as pt,attachChartTip as xt,makeTestSelect as ht,makeWaferSelect as bt,chartFillHeight as gt,applyCanvasFlow as yt,drawAxisUnit as wt,resolveChartCanvasColors as St,makeAxisFormat as le,horizontalTickSpacing as Tt,VERTICAL_TICK_SPACING_PX as kt,makeSeriesLegendItem as Mt,prepareCanvas as vt,drawOffAxisLimits as _e,limitLines as Lt,hasBothLimitKinds as Ct,makeLimitsSelect as Ot,stackLabelRows as je,strokeLimitLine as Ge,fillTextOnHalo as Xe}from"./chartShell.js";const u=52,At=16,c=16,Pt=44;export const NO_BIN_CATEGORY="none";export function binCategoryOf(x){return x===void 0?NO_BIN_CATEGORY:String(x)}export function binCategories(x){const te=new Set;let k=!1;for(const N of x)N.hbin===void 0?k=!0:te.add(N.hbin);const L=Array.from(te).sort((N,ae)=>N-ae).map(String);return k&&L.push(NO_BIN_CATEGORY),L}export function renderScatterPanel(x){const{title:te="Test scatter",items:k,testDefs:L,binColors:N,onSaveImage:ae,groups:_}=x,{card:l,body:R,controlsRow:re}=mt(te,ae,x.ownerDocument),ne=L.filter(e=>e.testNumber!==void 0),C=!!(_&&_.length>0),ye=(_??[]).map(e=>e.key),Ye=new Map(ye.map((e,n)=>[e,n]));let f=x.xTestNumber??ne[0]?.testNumber??null,w=x.yTestNumber??ne[1]?.testNumber??f,j=null;const we=()=>j!==null?k[j]?[k[j]]:[]:k;function Se(e,n,o){const a=l.ownerDocument.createElement("label");Object.assign(a.style,{display:"inline-flex",alignItems:"center",gap:D.xs,fontSize:ge.body,color:ee.label});const g=l.ownerDocument.createElement("span");g.textContent=e;const r=ht(ne,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:l.ownerDocument});return a.append(g,r),{wrap:a,select:r}}const{wrap:Ue,select:Ke}=Se("X:",f,e=>{f=e,K()}),{wrap:Ve,select:qe}=Se("Y:",w,e=>{w=e,K()});re.append(Ue,Ve);let O=x.axisPrefs??{clipOutliers:!1};const Te=l.ownerDocument.createElement("span");re.appendChild(Te);function ke(){const e=[f,w].map(n=>L.find(o=>o.testNumber===n));Te.replaceChildren(...e.some(Ct)?[Ot(O.limits??"both",n=>{O={...O,limits:n},x.onAxisPrefsChange?.(O),v()},l.ownerDocument)]:[])}!C&&k.length>1&&re.appendChild(bt(k,j,e=>{j=e,K()},{ownerDocument:l.ownerDocument}));const H=l.ownerDocument.createElement("div");H.dataset.wmapCaption="1",Object.assign(H.style,{color:ee.warnText,background:ee.warnBg,border:`1px solid ${ee.warnBorder}`,borderRadius:dt.control,padding:`${D.xs} ${D.md}`,fontSize:ge.body,marginBottom:D.xs,display:"none"}),l.insertBefore(H,R);function Me(){const e=C?[]:ct(we(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);e.length>0?(H.textContent=`\u26A0 Mixed ${e.join(", ")} across these points \u2014 trends here may be confounded (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,H.style.display=""):H.style.display="none"}Me();const oe=l.ownerDocument.createElement("div");oe.dataset.wmapCaption="1",Object.assign(oe.style,{color:ee.label,fontSize:ge.body,marginBottom:D.xs}),l.insertBefore(oe,R);function G(){const e=(C?"One point per die \xB7 coloured by group \xB7 click legend to filter":"One point per die across all wafers \xB7 coloured by hard bin \xB7 click legend to filter")+(fe?` \xB7 click a point to ${Ce}`:"")+(de?M.size>0?` \xB7 ${M.size.toLocaleString()} selected (click empty space to clear)`:" \xB7 drag to select dies":""),n=h.size===0?b:b.filter(g=>h.has(B(g))),{r:o,n:a}=rt(n);oe.textContent=o===null?`${e} \xB7 n = ${a.toLocaleString()}`:`${e} \xB7 r = ${o.toFixed(3)} \xB7 n = ${a.toLocaleString()}`}const B=e=>C?e.group??"\u2014":binCategoryOf(e.hbin),ve=e=>C?st(Ye.get(e)??0):e===NO_BIN_CATEGORY?Ne:N?.get(Number(e))??Ne,ce=e=>C?e:e===NO_BIN_CATEGORY?"No bin data":`Bin ${e}`,h=new Set,X=l.ownerDocument.createElement("div");Object.assign(X.style,{display:"flex",flexWrap:"wrap",gap:D.sm,marginTop:D.md,marginBottom:D.xl}),R.appendChild(X);const A=l.ownerDocument.createElement("canvas");A.style.display="block",A.style.cursor="crosshair",R.appendChild(A);const Le=pt(l);let b=[],y=0,P=1,S=0,E=1,Y=[],ie=null;const M=new Set,de=x.onSelect,fe=x.onOpen,Ce=x.openActionLabel??"open this wafer",Oe=new Map;for(const e of[...k,...(_??[]).flatMap(n=>n.items)])e.waferIndex!==void 0&&e.label&&Oe.set(e.waferIndex,e.label);function U(e){const n=L.find(o=>o.testNumber===e);return{unit:n?.unit,lines:Lt(n,O.limits)}}function Ae(){const e=R.clientWidth,n=Math.max(200,Math.min(400,e*.65)),o=gt(l,R,A,n);return{w:e,h:o,plotW:Math.max(10,e-u-At),plotH:Math.max(10,o-c-Pt)}}const me=new Map;function Pe(){for(const[e,n]of me){const o=h.has(e);n.setState({selected:o,dimmed:h.size>0&&!o})}}function $e(e){X.innerHTML="",me.clear(),h.clear();for(const n of e){const o=Mt(ce(n),ve(n),l.ownerDocument),a=o.el;a.dataset.cat=n,me.set(n,o),xt(a,l,Le,`${ce(n)} \u2014 click to filter`),a.addEventListener("click",()=>{h.has(n)?h.delete(n):h.add(n),Pe(),G(),v()}),X.appendChild(a)}Pe()}function v(){yt(A,X);const e=St(l),n=P-y,o=E-S,{w:a,h:g,plotW:r,plotH:s}=Ae(),W=vt(A,l,a,g);if(!W)return;const{ctx:t}=W;if(b.length===0){Y=[],t.font=`${z()}px system-ui, sans-serif`,t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="middle",t.fillText("No data \u2014 select two parametric tests with values.",a/2,g/2);return}t.font=`${z(-1)}px system-ui, sans-serif`;const T=f!==null?U(f).unit:void 0,V=Fe(y,P,r,Tt(t,Math.max(Math.abs(y),Math.abs(P)),T)),q=Fe(S,E,s,()=>kt),ue=le(Math.max(Math.abs(y),Math.abs(P)),f!==null?U(f).unit:void 0,V.step||void 0),pe=le(Math.max(Math.abs(S),Math.abs(E)),w!==null?U(w).unit:void 0,q.step||void 0);t.font=`${z(-1)}px system-ui, sans-serif`,t.strokeStyle=e.grid,t.lineWidth=.5,t.fillStyle=e.textMuted;for(const m of V.ticks){const d=u+(m-y)/n*r;t.beginPath(),t.moveTo(d,c),t.lineTo(d,c+s),t.stroke(),t.textAlign="center",t.textBaseline="top",t.fillText(ue.tick(m),d,c+s+4)}for(const m of q.ticks){const d=c+(1-(m-S)/o)*s;t.beginPath(),t.moveTo(u,d),t.lineTo(u+r,d),t.stroke(),t.textAlign="right",t.textBaseline="middle",t.fillText(pe.tick(m),u-4,d)}ue.unitLabel&&wt(t,ue.unitLabel,u+r/2,c+s+24,e.textMuted),pe.unitLabel&&(t.save(),t.font=`${z(-1)}px system-ui, sans-serif`,t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="middle",t.translate(6,c+s/2),t.rotate(-Math.PI/2),t.fillText(`(${pe.unitLabel})`,0,0),t.restore());const J=h.size===0?b:b.filter(m=>h.has(B(m))),De=J.length>5e3?Math.ceil(J.length/5e3):1;t.globalAlpha=Math.max(.15,Math.min(.7,200/(J.length/De))),Y=[];for(let m=0;m<J.length;m+=De){const d=J[m],$=u+(d.x-y)/n*r,I=c+(1-(d.y-S)/o)*s;Y.push({p:d,cx:$,cy:I}),t.beginPath(),t.arc($,I,2.5,0,Math.PI*2),t.fillStyle=ve(B(d)),t.fill()}t.globalAlpha=1;const xe=ie?Y.find(m=>m.p===ie):void 0;if(xe&&(t.beginPath(),t.arc(xe.cx,xe.cy,5,0,Math.PI*2),t.lineWidth=2,t.strokeStyle=e.text,t.stroke()),M.size>0){t.lineWidth=1.5,t.strokeStyle=e.text;let m=0;for(const d of M){if(h.size>0&&!h.has(B(d)))continue;if(++m>5e3)break;const $=u+(d.x-y)/n*r,I=c+(1-(d.y-S)/o)*s;t.beginPath(),t.arc($,I,4,0,Math.PI*2),t.stroke()}}if(f!==null&&w!==null){const m=U(f),d=U(w);t.strokeStyle=e.limitLine,t.fillStyle=e.limitLine,t.lineWidth=1,t.font=`${z(-1)}px system-ui, sans-serif`,t.textBaseline="top";const $=z(-1)+2,I=m.lines.filter(i=>i.value>=y&&i.value<=P).map(i=>{const p=u+(i.value-y)/n*r;return{l:i,cx:p,start:p+2,end:p+2+t.measureText(i.label).width}}),Be=je(I);I.forEach(({l:i,cx:p},Q)=>{Ge(t,i,e.limitLine,p,c,p,c+s),t.textAlign="left",Xe(t,i.label,p+2,c+2+Be[Q]*$,e.bg)});const he=d.lines.filter(i=>i.value>=S&&i.value<=E).map(i=>{const p=c+(1-(i.value-S)/o)*s;return{l:i,cy:p,start:p+2,end:p+2+z(-1)}}),nt=je(he,1),ot=se(he.map(i=>t.measureText(i.l.label).width))+8;he.forEach(({l:i,cy:p},Q)=>{Ge(t,i,e.limitLine,u,p,u+r,p),t.textAlign="left",Xe(t,i.label,u+3+nt[Q]*ot,p+2,e.bg)});const Ee=(i,p,Q)=>i.filter(F=>F.value<p||F.value>Q).map(F=>({value:F.value,label:F.label,side:F.value<p?"lo":"hi"})),We={left:u,right:u+r,top:c,bottom:c+s},be=Ee(m.lines,y,P),ze=c+(I.length?(se(Be)+1)*$:0),it=Math.max(be.filter(i=>i.side==="lo").length,be.filter(i=>i.side==="hi").length);_e(t,be,{...We,top:ze},"horizontal",e.limitLine,i=>le(Math.abs(i),m.unit).tick(i),e.bg),_e(t,Ee(d.lines,S,E),{...We,top:ze+it*$},"vertical",e.limitLine,i=>le(Math.abs(i),d.unit).tick(i),e.bg),t.setLineDash([])}t.strokeStyle=e.border,t.lineWidth=1,t.beginPath(),t.moveTo(u,c),t.lineTo(u,c+s),t.moveTo(u,c+s),t.lineTo(u+r,c+s),t.stroke()}function K(){if(ie=null,M.clear(),Me(),ke(),ne.length<2||f===null||w===null){b=[],$e([]),G(),v();return}b=C?at(_,f,w):lt(we(),f,w);let e;if(C){const n=new Set(b.map(B));e=ye.filter(o=>n.has(o))}else e=binCategories(b);if($e(e),G(),b.length>0){const n=b.map(T=>T.x),o=b.map(T=>T.y),a=Re(n),g=se(n),r=Re(o),s=se(o),W=(g-a)*.05||1,t=(s-r)*.05||1;y=a-W,P=g+W,S=r-t,E=s+t}v()}const Ie=e=>!!fe&&e.waferIndex!==void 0;function Je(e){const n=L.find(s=>s.testNumber===f),o=L.find(s=>s.testNumber===w),a=e.waferIndex!==void 0?Oe.get(e.waferIndex):void 0,g=e.die&&e.die.x!==void 0?`die (${e.die.x}, ${e.die.y})`:"",r=[a,g].filter(Boolean).join(" \xB7 ");return(r?`<strong>${Z(r)}</strong><br>`:"")+Z(`${n?.name??"X"}: ${He(e.x,n?.unit)}`)+"<br>"+Z(`${o?.name??"Y"}: ${He(e.y,o?.unit)}`)+"<br>"+Z(ce(B(e)))+(Ie(e)?`<br><em>click to ${Z(Ce)}</em>`:"")}const Qe=ft({card:l,canvas:A,tooltip:Le,drawn:()=>Y,describe:Je,clickable:Ie,onOpen:e=>{f!==null&&fe(e.waferIndex,f)},onHover:e=>{ie=e,v()},select:de?{pick:({x0:e,y0:n,x1:o,y1:a})=>{const{plotW:g,plotH:r}=Ae(),s=P-y,W=E-S;return(h.size===0?b:b.filter(T=>h.has(B(T)))).filter(T=>{const V=u+(T.x-y)/s*g,q=c+(1-(T.y-S)/W)*r;return V>=e&&V<=o&&q>=n&&q<=a})},onPicked:(e,n)=>{M.clear();for(const o of e)M.add(o);G(),v(),e.length>0&&f!==null&&de(e,n,A,f)},onClear:()=>{M.clear(),G(),v()},hasSelection:()=>M.size>0}:void 0}),Ze=ut(l,()=>v());K();function et(e,n){f=e,w=n,Ke.value=String(e),qe.value=String(n),K()}function tt(e){if((e.limits??"both")===(O.limits??"both")){O=e;return}O=e,ke(),v()}return{card:l,setXY:et,setAxisPrefs:tt,destroy:()=>{Ze.disconnect(),Qe.destroy()}}}
@@ -19,6 +19,14 @@ export interface SweepPanelOptions {
19
19
  population?: string;
20
20
  testDefs: TestDef[] | undefined;
21
21
  onSaveImage?: SaveImageHandler;
22
+ /**
23
+ * Click a level of the curve: the tests measured there (one per series that has a value at it) and its label, with the
24
+ * click, so the host can pick out the dies that were measured and open a menu on them.
25
+ */
26
+ onSelectPoint?: (sel: {
27
+ testNumbers: number[];
28
+ label: string;
29
+ }, e: MouseEvent) => void;
22
30
  ownerDocument?: Document;
23
31
  }
24
32
  export interface SweepPanelHandle {
@@ -26,6 +34,10 @@ export interface SweepPanelHandle {
26
34
  destroy: () => void;
27
35
  /** Re-aggregate over a new population, keeping the same curve definition. */
28
36
  setDies: (dies: Die[]) => void;
37
+ /** Draw a changed definition (the editor), keeping the population. */
38
+ setSpec: (spec: SweepSpec) => void;
39
+ /** The row under the title, for the host's own buttons (Edit, Duplicate, Delete). */
40
+ actions: HTMLElement;
29
41
  }
30
42
  export declare function renderSweepPanel(options: SweepPanelOptions): SweepPanelHandle;
31
43
  //# sourceMappingURL=sweep.d.ts.map
@@ -1 +1 @@
1
- import{buildSweepData as ce,sweepAppliesTo as fe}from"../../stats/sweep.js";import{fmt as C}from"../../renderer/fmt.js";import{SPACE as ne,fontPx as pe,FONT as oe,CLR as G,ALPHA as de}from"../toolbar.js";import{categorical as he}from"./palette.js";import{cardShell as me,observeResize as ue,makeTooltip as xe,positionChartTooltip as ge,renderEmptyState as se,chartFillHeight as be,populationPhrase as ye,PADDING as Y,prepareCanvas as $e,makeAxisFormat as Te,VERTICAL_TICK_SPACING_PX as ve}from"./chartShell.js";import{fitTicks as Se}from"../../renderer/axisTicks.js";import{escHtml as m}from"../../core/utils.js";import{unmarkedLabel as Me,derivedTestNote as Le,DERIVED_MARK as J,DERIVED_KEY as we}from"../../renderer/testLabel.js";function _(u,a){return a?C(u,a).replace(/(\.\d*?)0+(?=\D|$)/,"$1").replace(/\.(?=\D|$)/,""):Number.isInteger(u)?String(u):String(Number(u.toPrecision(3)))}function Pe(u){const a=Me(u.label),S=a.length>9?`${a.slice(0,8)}\u2026`:a;return u.derived?`${J} ${S}`:S}const ie=260,ke=62,Ae=56,Ee=3;function Ie(u,a){const S=u.series[0]?.points[0]?.testNumber;return S===void 0?void 0:a?.find(j=>j.testNumber===S)?.unit}export function renderSweepPanel(u){const{spec:a,testDefs:S,onSaveImage:j,population:re}=u;let z=u.dies;const{card:y,body:M}=me(a.title,j,u.ownerDocument);y.style.alignSelf="start";const V=y.ownerDocument,N=V.createElement("div");Object.assign(N.style,{color:G.label,fontSize:oe.body,marginBottom:ne.sm}),y.insertBefore(N,M);const W=V.createElement("div");Object.assign(W.style,{color:G.label,fontSize:oe.body,marginTop:ne.sm}),y.appendChild(W);const L=xe(y);let X=null,E=null,$;const H=n=>a.series[n]?.color??he(n);function Q(){if(M.innerHTML="",W.innerHTML="",X?.disconnect(),X=null,a.series.length===0){N.textContent="",se(M,"This sweep defines no series.");return}if(E=ce(z,S,a),$=Ie(E,S),E.series.filter(i=>i.points.some(T=>T.count>0)).length===0){N.textContent="",se(M,fe(a,S)?"No values recorded for the tests in this sweep.":"None of this sweep\u2019s tests are in this data \u2014 it may belong to another test program."),Z();return}N.textContent=`Line = median across ${ye(E.dieCount,z.length,re)}, band = p10\u2013p90`+(a.series.length>2?" \xB7 crossing and separation measured between the first two series":"");const c=V.createElement("canvas");c.style.display="block",M.appendChild(c);let o=null;const w=()=>{const i=E;if(i===null)return;const T=Math.max(1,M.clientWidth),B=Math.max(ie,be(y,M,c,ie)),F=$e(c,y,T,B);if(!F)return;const{ctx:e,theme:x}=F,g=t=>i.xScale==="log"?Math.log10(t):t;let f=1/0,h=-1/0,b=1/0,v=-1/0;for(const t of i.series)for(const s of t.points)s.count!==0&&(f=Math.min(f,s.p10),h=Math.max(h,s.p90),b=Math.min(b,g(s.x)),v=Math.max(v,g(s.x)));for(const t of i.separations)f=Math.min(f,t.y),h=Math.max(h,t.y);if(!Number.isFinite(f)||!Number.isFinite(h))return;f===h&&(f-=1,h+=1);const l=(h-f)*.08;if(f-=l,h+=l,b===v&&(b-=.5,v+=.5),i.xScale==="log"){const t=(v-b)*.03;b-=t,v+=t}e.font=`${pe(-1)}px system-ui, sans-serif`;const r=Y+ke,p=T-Y-8,R=Y,P=B-Ae,k=t=>P-(t-f)/(h-f)*(P-R),D=t=>r+(g(t)-b)/(v-b)*(p-r);o={left:r,right:p,xLo:b,xHi:v,yOf:k,ax:g},e.strokeStyle=x.border,e.lineWidth=1,e.beginPath(),e.moveTo(r,R),e.lineTo(r,P),e.lineTo(p,P),e.stroke(),e.fillStyle=x.text,e.textAlign="right",e.textBaseline="middle";const q=Se(f,h,P-R,()=>ve),le=Te(Math.max(Math.abs(f),Math.abs(h)),$,q.step||void 0);for(const t of q.ticks)e.fillText(le.tickWithUnit(t),r-6,k(t));for(const t of i.separations){const s=k(t.y);e.save(),e.strokeStyle=x.limitLine,e.globalAlpha=.55,e.setLineDash([4,3]),e.beginPath(),e.moveTo(r,s),e.lineTo(p,s),e.stroke(),e.restore()}if(i.series.forEach((t,s)=>{const A=t.points.filter(d=>d.count>0);if(!(A.length<2)){e.save(),e.fillStyle=H(s),e.globalAlpha=.14,e.beginPath(),A.forEach((d,I)=>{const O=D(d.x),te=k(d.p90);I===0?e.moveTo(O,te):e.lineTo(O,te)});for(let d=A.length-1;d>=0;d--){const I=A[d];e.lineTo(D(I.x),k(I.p10))}e.closePath(),e.fill(),e.restore()}}),i.series.forEach((t,s)=>{e.strokeStyle=H(s),e.lineWidth=1.75,e.beginPath();let A=!1;for(const d of t.points){if(d.count===0){A=!1;continue}const I=D(d.x),O=k(d.median);A?e.lineTo(I,O):e.moveTo(I,O),A=!0}e.stroke(),e.fillStyle=H(s);for(const d of t.points)d.count!==0&&(e.beginPath(),e.arc(D(d.x),k(d.median),Ee,0,Math.PI*2),e.fill())}),i.crossing){const t=D(i.crossing.x),s=k(i.crossing.y);e.save(),e.strokeStyle=x.text,e.globalAlpha=.45,e.setLineDash([3,3]),e.beginPath(),e.moveTo(t,s),e.lineTo(t,P),e.stroke(),e.restore(),e.save(),e.strokeStyle=x.text,e.lineWidth=2,e.beginPath(),e.arc(t,s,5.5,0,Math.PI*2),e.stroke(),e.restore()}e.fillStyle=x.text,e.textBaseline="top",e.textAlign="center";const K=i.xIsPhysical?[...new Set(i.series.flatMap(t=>t.points.map(s=>s.x)))].sort((t,s)=>t-s).map(t=>({x:t,label:_(t,i.xUnit)})):(i.series[0]?.points??[]).map(t=>({x:t.x,label:Pe(t)})),ae=Math.max(1,Math.ceil(K.length*46/Math.max(1,p-r)));K.forEach((t,s)=>{s%ae!==0&&s!==K.length-1||e.fillText(t.label,D(t.x),P+6)}),e.save(),e.fillStyle=x.text,e.globalAlpha=.75,e.textAlign="center",e.textBaseline="bottom",e.fillText(i.xLabel,(r+p)/2,B-4),e.restore(),e.save(),e.fillStyle=x.text,e.globalAlpha=.75,e.translate(12,(R+P)/2),e.rotate(-Math.PI/2),e.textBaseline="top",e.fillText(i.yLabel+($?` (${$})`:""),0,0),e.restore(),e.textAlign="left",e.textBaseline="middle";let U=r+4;const ee=R+8;i.series.forEach((t,s)=>{e.fillStyle=H(s),e.fillRect(U,ee-4,10,3),U+=14,e.fillStyle=x.text,e.fillText(t.label,U,ee),U+=e.measureText(t.label).width+16})};w(),X=ue(M,w),c.addEventListener("mousemove",i=>{const T=E;if(T===null||o===null){L.style.display="none";return}const B=c.getBoundingClientRect(),F=i.clientX-B.left,e=o.xLo+(F-o.left)/Math.max(1,o.right-o.left)*(o.xHi-o.xLo),x=o.ax;let g=null;for(const l of T.series)for(const r of l.points){if(r.count===0)continue;const p=Math.abs(x(r.x)-e);(g===null||p<g.dist)&&(g={x:r.x,dist:p})}if(g===null){L.style.display="none";return}const f=T.series.map(l=>l.points.find(r=>r.x===g.x&&r.count>0)),h=T.series.map((l,r)=>{const p=f[r];return p===void 0?"":`<span style="color:${H(r)}">\u25A0</span> ${p.derived?`${J} `:""}${m(l.label)}: <strong>${m(C(p.median,$))}</strong><br><span style="opacity:${de.muted}">p10 ${m(C(p.p10,$))} \u2013 p90 ${m(C(p.p90,$))} \xB7 n=${p.count.toLocaleString()}</span>`}).filter(Boolean);if(h.length===0){L.style.display="none";return}const b=T.series.flatMap(l=>l.points).find(l=>l.x===g.x&&l.count>0),v=[...new Set(f.map(l=>Le(l)).filter(l=>l!==void 0))];L.innerHTML=`<strong>${m(b?.label??"")}</strong><br>`+h.join("<br>")+v.map(l=>`<br><em>${m(l)}</em>`).join(""),L.style.display="block",ge(L,y,i.clientX,i.clientY)}),c.addEventListener("mouseleave",()=>{L.style.display="none"}),Z()}function Z(){const n=E;if(n===null)return;const c=[];n.notMeasured!==void 0?c.push(`<strong>Crossing and widths:</strong> not measured \u2014 ${m(n.notMeasured)}`):n.crossing?c.push(`<strong>Crossing:</strong> ${m(`${n.xLabel} = ${_(n.crossing.x,n.xUnit)}`)} \xB7 ${m(`${n.yLabel} ${C(n.crossing.y,$)}`)}`+(n.crossing.multiple?" <em>(first of several \u2014 the curves cross more than once)</em>":"")):(a.crossing??!0)&&a.series.length>=2&&c.push("<strong>Crossing:</strong> the first two series do not cross over the swept range");for(const o of n.separations)if(o.distance!==null){const w=o.ratio!==void 0&&o.from!==void 0&&o.to!==void 0?`\xD7${Number(o.ratio.toPrecision(3))} on the ${n.xLabel} axis (${_(o.from,n.xUnit)} \u2192 ${_(o.to,n.xUnit)})`:`${_(o.distance,n.xUnit)} on the ${n.xLabel} axis`;c.push(`<strong>Width at ${m(C(o.y,$))}:</strong> ${m(w)}`)}else{const w=o.reason==="second-series-never-reaches"?n.series[1]?.label:n.series[0]?.label;c.push(`<strong>Width at ${m(C(o.y,$))}:</strong> not measurable \u2014 ${m(w??"a series")} never reaches this level`)}n.series.some(o=>o.points.some(w=>w.derived))&&c.push(`${J} ${m(we)} \u2014 hover a point for the expression`);for(const o of n.warnings)c.push(`<span style="color:${G.errText}">${m(o)}</span>`);W.innerHTML=c.join("<br>")}return Q(),{card:y,destroy:()=>{X?.disconnect(),L.remove(),y.remove()},setDies:n=>{z=n,Q()}}}
1
+ import{buildSweepData as de,sweepAppliesTo as ue}from"../../stats/sweep.js";import{fmt as N}from"../../renderer/fmt.js";import{SPACE as oe,fontPx as he,FONT as ie,CLR as Y,ALPHA as me}from"../toolbar.js";import{categorical as xe}from"./palette.js";import{cardShell as ge,observeResize as be,makeTooltip as ye,positionChartTooltip as $e,renderEmptyState as re,chartFillHeight as Te,populationPhrase as ve,PADDING as J,prepareCanvas as Se,makeAxisFormat as Me,VERTICAL_TICK_SPACING_PX as Le}from"./chartShell.js";import{fitTicks as we}from"../../renderer/axisTicks.js";import{escHtml as m}from"../../core/utils.js";import{unmarkedLabel as Pe,derivedTestNote as ke,DERIVED_MARK as Q,DERIVED_KEY as Ae}from"../../renderer/testLabel.js";function O(c,x){return x?N(c,x).replace(/(\.\d*?)0+(?=\D|$)/,"$1").replace(/\.(?=\D|$)/,""):Number.isInteger(c)?String(c):String(Number(c.toPrecision(3)))}function Ce(c){const x=Pe(c.label),E=x.length>9?`${x.slice(0,8)}\u2026`:x;return c.derived?`${Q} ${E}`:E}const le=260,Ee=62,Ie=56,Ne=3;function De(c,x){const E=c.series[0]?.points[0]?.testNumber;return E===void 0?void 0:x?.find(j=>j.testNumber===E)?.unit}export function renderSweepPanel(c){const{testDefs:x,onSaveImage:E,population:j}=c;let S=c.spec,z=c.dies;const{card:g,body:L,heading:ae,controlsRow:ce}=ge(S.title,E,c.ownerDocument);g.style.alignSelf="start";const V=g.ownerDocument,D=V.createElement("div");D.dataset.wmapCaption="1",Object.assign(D.style,{color:Y.label,fontSize:ie.body,marginBottom:oe.sm}),g.insertBefore(D,L);const W=V.createElement("div");Object.assign(W.style,{color:Y.label,fontSize:ie.body,marginTop:oe.sm}),g.appendChild(W);const w=ye(g);let F=null,M=null,$;const R=n=>S.series[n]?.color??xe(n);function K(){if(L.innerHTML="",W.innerHTML="",F?.disconnect(),F=null,S.series.length===0){D.textContent="",re(L,"This sweep defines no series.");return}if(M=de(z,x,S),$=De(M,x),M.series.filter(s=>s.points.some(d=>d.count>0)).length===0){D.textContent="",re(L,ue(S,x)?"No values recorded for the tests in this sweep.":"None of this sweep\u2019s tests are in this data \u2014 it may belong to another test program."),Z();return}D.textContent=`Line = median across ${ve(M.dieCount,z.length,j)}, band = p10\u2013p90`+(S.series.length>2?" \xB7 crossing and separation measured between the first two series":"")+(c.onSelectPoint?" \xB7 click a level to chart or tabulate the dies measured there":"");const l=V.createElement("canvas");l.style.display="block",L.appendChild(l);let i=null;const P=()=>{const s=M;if(s===null)return;const d=Math.max(1,L.clientWidth),b=Math.max(le,Te(g,L,l,le)),y=Se(l,g,d,b);if(!y)return;const{ctx:e,theme:f}=y,T=t=>s.xScale==="log"?Math.log10(t):t;let a=1/0,o=-1/0,u=1/0,h=-1/0;for(const t of s.series)for(const r of t.points)r.count!==0&&(a=Math.min(a,r.p10),o=Math.max(o,r.p90),u=Math.min(u,T(r.x)),h=Math.max(h,T(r.x)));for(const t of s.separations)a=Math.min(a,t.y),o=Math.max(o,t.y);if(!Number.isFinite(a)||!Number.isFinite(o))return;a===o&&(a-=1,o+=1);const ee=(o-a)*.08;if(a-=ee,o+=ee,u===h&&(u-=.5,h+=.5),s.xScale==="log"){const t=(h-u)*.03;u-=t,h+=t}e.font=`${he(-1)}px system-ui, sans-serif`;const v=J+Ee,H=d-J-8,X=J,k=b-Ie,A=t=>k-(t-a)/(o-a)*(k-X),B=t=>v+(T(t)-u)/(h-u)*(H-v);i={left:v,right:H,xLo:u,xHi:h,yOf:A,ax:T},e.strokeStyle=f.border,e.lineWidth=1,e.beginPath(),e.moveTo(v,X),e.lineTo(v,k),e.lineTo(H,k),e.stroke(),e.fillStyle=f.text,e.textAlign="right",e.textBaseline="middle";const te=we(a,o,k-X,()=>Le),fe=Me(Math.max(Math.abs(a),Math.abs(o)),$,te.step||void 0);for(const t of te.ticks)e.fillText(fe.tickWithUnit(t),v-6,A(t));for(const t of s.separations){const r=A(t.y);e.save(),e.strokeStyle=f.limitLine,e.globalAlpha=.55,e.setLineDash([4,3]),e.beginPath(),e.moveTo(v,r),e.lineTo(H,r),e.stroke(),e.restore()}if(s.series.forEach((t,r)=>{const C=t.points.filter(p=>p.count>0);if(!(C.length<2)){e.save(),e.fillStyle=R(r),e.globalAlpha=.14,e.beginPath(),C.forEach((p,I)=>{const _=B(p.x),se=A(p.p90);I===0?e.moveTo(_,se):e.lineTo(_,se)});for(let p=C.length-1;p>=0;p--){const I=C[p];e.lineTo(B(I.x),A(I.p10))}e.closePath(),e.fill(),e.restore()}}),s.series.forEach((t,r)=>{e.strokeStyle=R(r),e.lineWidth=1.75,e.beginPath();let C=!1;for(const p of t.points){if(p.count===0){C=!1;continue}const I=B(p.x),_=A(p.median);C?e.lineTo(I,_):e.moveTo(I,_),C=!0}e.stroke(),e.fillStyle=R(r);for(const p of t.points)p.count!==0&&(e.beginPath(),e.arc(B(p.x),A(p.median),Ne,0,Math.PI*2),e.fill())}),s.crossing){const t=B(s.crossing.x),r=A(s.crossing.y);e.save(),e.strokeStyle=f.text,e.globalAlpha=.45,e.setLineDash([3,3]),e.beginPath(),e.moveTo(t,r),e.lineTo(t,k),e.stroke(),e.restore(),e.save(),e.strokeStyle=f.text,e.lineWidth=2,e.beginPath(),e.arc(t,r,5.5,0,Math.PI*2),e.stroke(),e.restore()}e.fillStyle=f.text,e.textBaseline="top",e.textAlign="center";const G=s.xIsPhysical?[...new Set(s.series.flatMap(t=>t.points.map(r=>r.x)))].sort((t,r)=>t-r).map(t=>({x:t,label:O(t,s.xUnit)})):(s.series[0]?.points??[]).map(t=>({x:t.x,label:Ce(t)})),pe=Math.max(1,Math.ceil(G.length*46/Math.max(1,H-v)));G.forEach((t,r)=>{r%pe!==0&&r!==G.length-1||e.fillText(t.label,B(t.x),k+6)}),e.save(),e.fillStyle=f.text,e.globalAlpha=.75,e.textAlign="center",e.textBaseline="bottom",e.fillText(s.xLabel,(v+H)/2,b-4),e.restore(),e.save(),e.fillStyle=f.text,e.globalAlpha=.75,e.translate(12,(X+k)/2),e.rotate(-Math.PI/2),e.textBaseline="top",e.fillText(s.yLabel+($?` (${$})`:""),0,0),e.restore(),e.textAlign="left",e.textBaseline="middle";let U=v+4;const ne=X+8;s.series.forEach((t,r)=>{e.fillStyle=R(r),e.fillRect(U,ne-4,10,3),U+=14,e.fillStyle=f.text,e.fillText(t.label,U,ne),U+=e.measureText(t.label).width+16})};P(),F=be(L,P);const q=s=>{const d=M;if(d===null||i===null)return null;const b=s.clientX-l.getBoundingClientRect().left,y=i.xLo+(b-i.left)/Math.max(1,i.right-i.left)*(i.xHi-i.xLo);let e=null;for(const f of d.series)for(const T of f.points){if(T.count===0)continue;const a=Math.abs(i.ax(T.x)-y);(e===null||a<e.dist)&&(e={x:T.x,dist:a})}return e===null?null:e.x};l.addEventListener("mousemove",s=>{const d=M,b=q(s);if(d===null||b===null){w.style.display="none";return}const y={x:b},e=d.series.map(o=>o.points.find(u=>u.x===y.x&&u.count>0)),f=d.series.map((o,u)=>{const h=e[u];return h===void 0?"":`<span style="color:${R(u)}">\u25A0</span> ${h.derived?`${Q} `:""}${m(o.label)}: <strong>${m(N(h.median,$))}</strong><br><span style="opacity:${me.muted}">p10 ${m(N(h.p10,$))} \u2013 p90 ${m(N(h.p90,$))} \xB7 n=${h.count.toLocaleString()}</span>`}).filter(Boolean);if(f.length===0){w.style.display="none";return}const T=d.series.flatMap(o=>o.points).find(o=>o.x===y.x&&o.count>0),a=[...new Set(e.map(o=>ke(o)).filter(o=>o!==void 0))];w.innerHTML=`<strong>${m(T?.label??"")}</strong><br>`+f.join("<br>")+a.map(o=>`<br><em>${m(o)}</em>`).join(""),w.style.display="block",$e(w,g,s.clientX,s.clientY)}),l.addEventListener("mouseleave",()=>{w.style.display="none"}),c.onSelectPoint&&(l.style.cursor="pointer",l.addEventListener("click",s=>{const d=M,b=q(s);if(d===null||b===null)return;const y=d.series.flatMap(e=>e.points.filter(f=>f.x===b&&f.count>0));y.length!==0&&(w.style.display="none",c.onSelectPoint({testNumbers:y.map(e=>e.testNumber),label:y[0].label},s))})),Z()}function Z(){const n=M;if(n===null)return;const l=[];n.notMeasured!==void 0?l.push(`<strong>Crossing and widths:</strong> not measured \u2014 ${m(n.notMeasured)}`):n.crossing?l.push(`<strong>Crossing:</strong> ${m(`${n.xLabel} = ${O(n.crossing.x,n.xUnit)}`)} \xB7 ${m(`${n.yLabel} ${N(n.crossing.y,$)}`)}`+(n.crossing.multiple?" <em>(first of several \u2014 the curves cross more than once)</em>":"")):(S.crossing??!0)&&S.series.length>=2&&l.push("<strong>Crossing:</strong> the first two series do not cross over the swept range");for(const i of n.separations)if(i.distance!==null){const P=i.ratio!==void 0&&i.from!==void 0&&i.to!==void 0?`\xD7${Number(i.ratio.toPrecision(3))} on the ${n.xLabel} axis (${O(i.from,n.xUnit)} \u2192 ${O(i.to,n.xUnit)})`:`${O(i.distance,n.xUnit)} on the ${n.xLabel} axis`;l.push(`<strong>Width at ${m(N(i.y,$))}:</strong> ${m(P)}`)}else{const P=i.reason==="second-series-never-reaches"?n.series[1]?.label:n.series[0]?.label;l.push(`<strong>Width at ${m(N(i.y,$))}:</strong> not measurable \u2014 ${m(P??"a series")} never reaches this level`)}n.series.some(i=>i.points.some(P=>P.derived))&&l.push(`${Q} ${m(Ae)} \u2014 hover a point for the expression`);for(const i of n.warnings)l.push(`<span style="color:${Y.errText}">${m(i)}</span>`);W.innerHTML=l.join("<br>")}return K(),{card:g,destroy:()=>{F?.disconnect(),w.remove(),g.remove()},setDies:n=>{z=n,K()},setSpec:n=>{S=n,ae.textContent=n.title,g.dataset.wmapChartTitle=n.title,K()},actions:ce}}
@@ -1,3 +1,4 @@
1
+ import { type TestPassKind } from '../../stats/testPassRate.js';
1
2
  import type { TestPassRateItem } from '../../stats/testPassRate.js';
2
3
  import type { TestDef } from '../../renderer/buildWaferMap.js';
3
4
  import { type SaveImageHandler } from './chartShell.js';
@@ -10,6 +11,12 @@ export interface TestPassRatePanelOptions {
10
11
  }[];
11
12
  testDefs: TestDef[] | undefined;
12
13
  onSaveImage?: SaveImageHandler;
14
+ /** Click a test's row: the host picks out the dies that fail it (judged the way the card is showing). */
15
+ onSelectTest?: (row: {
16
+ testNumber: number;
17
+ label: string;
18
+ kind: TestPassKind;
19
+ }, e: MouseEvent) => void;
13
20
  ownerDocument?: Document;
14
21
  }
15
22
  export interface TestPassRatePanelHandle {
@@ -1 +1 @@
1
- import{buildTestPassRateData as F,hasJudgeableTests as B}from"../../stats/testPassRate.js";import{categorical as I,QUANTITY as N}from"./palette.js";import{SPACE as H,FONT as _,CLR as S}from"../toolbar.js";import{cardShell as A,makeTooltip as O,makeSegmented as j,renderEmptyState as y}from"./chartShell.js";import{renderGroupedBarPlot as M}from"./groupedBarPlot.js";import{escHtml as g}from"../../core/utils.js";const k=100,G=8,K={spec:"Test limits",testFlag:"Tester flag",functional:"Functional"},U={spec:"Parametric pass rate \xB7 test limits",testFlag:"Parametric pass rate \xB7 tester flag",functional:"Functional test pass rate"};export function renderTestPassRatePanel(f){const{groups:u,testDefs:b,onSaveImage:$}=f,p=f.ownerDocument??document,P=u.flatMap(t=>t.items.flatMap(a=>a.dies??[])),d=["spec","testFlag","functional"].filter(t=>B(b,t,P));let s=d[0]??"spec";const h=t=>U[t],{card:o,heading:v,body:i,controlsRow:x}=A(h(s),$,p);o.style.alignSelf="start",d.length>1&&x.appendChild(j(d.map(t=>[t,K[t]]),s,t=>{s=t,v.textContent=h(s),T()},p));const n=p.createElement("div");Object.assign(n.style,{color:S.label,fontSize:_.body,marginBottom:H.sm}),o.insertBefore(n,i);const D=O(o);let m=null;function T(){if(m?.destroy(),m=null,i.innerHTML="",d.length===0){n.textContent="",y(i,"No tests with a pass/fail verdict \u2014 parametric tests need test limits, functional tests need recorded results.");return}const t=F(u,b,s);if(t.rows.length===0){n.textContent="",y(i,s==="spec"?"No parametric test has test limits to judge against.":s==="testFlag"?"No parametric test has a recorded tester verdict.":"No functional test has recorded results.");return}const a=t.groups.length>1,R=s==="spec"?"Dies within test limits, per test":s==="testFlag"?"Dies the tester marked pass, per test":"Dies passing the recorded verdict, per test";if(n.textContent=R+(a?" \xB7 one bar per group":"")+" \xB7 worst first",t.disagreementDies){const e=p.createElement("span");e.textContent=` \xB7 ${t.disagreementDies.toLocaleString()} die${t.disagreementDies===1?"":"s"} judged differently by limits and tester flag`,Object.assign(e.style,{color:S.warnText}),n.appendChild(e)}const L=t.rows,C=e=>a?I(e):N;m=M(o,i,D,{rows:L.map(e=>({label:e.label,bars:e.byGroup.map(l=>({fraction:l.passRatePercent===null?null:l.passRatePercent/100})),trailing:e.overall.passRatePercent===null?"\u2014":`${e.overall.passRatePercent.toFixed(1)}%`})),groups:t.groups,labelWidth:k,maxVisibleRows:G,showLegend:a,maxLabelChars:14,emptyBarText:"no data",colorOf:C,tooltipHtml:(e,l)=>{const r=L[e],c=r.byGroup[l],w=a?` \xB7 ${g(String(t.groups[l]))}`:"";if(c.passRatePercent===null)return`<strong>${g(r.label)}</strong>${w}<br>no dies with a verdict`;const E=s==="spec"&&!a&&(r.failLowDies||r.failHighDies)?`<br>${r.failLowDies??0} below the low limit \xB7 ${r.failHighDies??0} above the high limit`:"";return`<strong>${g(r.label)}</strong>${w}<br>${c.passRatePercent.toFixed(1)}% pass<br>${c.passDies.toLocaleString()} pass \xB7 ${c.failDies.toLocaleString()} fail \xB7 n = ${c.totalDies.toLocaleString()}`+E}})}return T(),{card:o,destroy:()=>{m?.destroy(),D.remove(),o.remove()}}}
1
+ import{buildTestPassRateData as F,hasJudgeableTests as N}from"../../stats/testPassRate.js";import{categorical as B,QUANTITY as I}from"./palette.js";import{SPACE as _,FONT as H,CLR as L}from"../toolbar.js";import{cardShell as k,makeTooltip as A,makeSegmented as O,renderEmptyState as y}from"./chartShell.js";import{renderGroupedBarPlot as j}from"./groupedBarPlot.js";import{escHtml as f}from"../../core/utils.js";const M=100,G=8,K={spec:"Test limits",testFlag:"Tester flag",functional:"Functional"},U={spec:"Parametric pass rate \xB7 test limits",testFlag:"Parametric pass rate \xB7 tester flag",functional:"Functional test pass rate"};export function renderTestPassRatePanel(i){const{groups:b,testDefs:h,onSaveImage:$}=i,p=i.ownerDocument??document,C=b.flatMap(t=>t.items.flatMap(o=>o.dies??[])),m=["spec","testFlag","functional"].filter(t=>N(h,t,C));let s=m[0]??"spec";const T=t=>U[t],{card:n,heading:P,body:c,controlsRow:R}=k(T(s),$,p);n.style.alignSelf="start",m.length>1&&R.appendChild(O(m.map(t=>[t,K[t]]),s,t=>{s=t,P.textContent=T(s),S()},p));const r=p.createElement("div");r.dataset.wmapCaption="1",Object.assign(r.style,{color:L.label,fontSize:H.body,marginBottom:_.sm}),n.insertBefore(r,c);const D=A(n);let g=null;function S(){if(g?.destroy(),g=null,c.innerHTML="",m.length===0){r.textContent="",y(c,"No tests with a pass/fail verdict \u2014 parametric tests need test limits, functional tests need recorded results.");return}const t=F(b,h,s);if(t.rows.length===0){r.textContent="",y(c,s==="spec"?"No parametric test has test limits to judge against.":s==="testFlag"?"No parametric test has a recorded tester verdict.":"No functional test has recorded results.");return}const o=t.groups.length>1,v=s==="spec"?"Dies within test limits, per test":s==="testFlag"?"Dies the tester marked pass, per test":"Dies passing the recorded verdict, per test";if(r.textContent=v+(o?" \xB7 one bar per group":"")+" \xB7 worst first"+(i.onSelectTest?" \xB7 click a test to chart or tabulate the dies that fail it":""),t.disagreementDies){const e=p.createElement("span");e.textContent=` \xB7 ${t.disagreementDies.toLocaleString()} die${t.disagreementDies===1?"":"s"} judged differently by limits and tester flag`,Object.assign(e.style,{color:L.warnText}),r.appendChild(e)}const u=t.rows,x=e=>o?B(e):I;g=j(n,c,D,{rows:u.map(e=>({label:e.label,bars:e.byGroup.map(l=>({fraction:l.passRatePercent===null?null:l.passRatePercent/100})),trailing:e.overall.passRatePercent===null?"\u2014":`${e.overall.passRatePercent.toFixed(1)}%`})),groups:t.groups,labelWidth:M,maxVisibleRows:G,showLegend:o,maxLabelChars:14,emptyBarText:"no data",colorOf:x,onRowClick:i.onSelectTest?(e,l,a)=>i.onSelectTest({testNumber:u[e].testNumber,label:u[e].label,kind:s},a):void 0,tooltipHtml:(e,l)=>{const a=u[e],d=a.byGroup[l],w=o?` \xB7 ${f(String(t.groups[l]))}`:"";if(d.passRatePercent===null)return`<strong>${f(a.label)}</strong>${w}<br>no dies with a verdict`;const E=s==="spec"&&!o&&(a.failLowDies||a.failHighDies)?`<br>${a.failLowDies??0} below the low limit \xB7 ${a.failHighDies??0} above the high limit`:"";return`<strong>${f(a.label)}</strong>${w}<br>${d.passRatePercent.toFixed(1)}% pass<br>${d.passDies.toLocaleString()} pass \xB7 ${d.failDies.toLocaleString()} fail \xB7 n = ${d.totalDies.toLocaleString()}`+E}})}return S(),{card:n,destroy:()=>{g?.destroy(),D.remove(),n.remove()}}}
@@ -11,6 +11,17 @@ export interface TrendPanelOptions {
11
11
  onOpen?: (key: number, testNumber: number) => void;
12
12
  /** Right-click on a wafer's point — see `WaferContextMenuHandler`. */
13
13
  onWaferContextMenu?: WaferContextMenuHandler;
14
+ /**
15
+ * Drag across the trend to pick a run of wafers: their `key`s (the wafer indexes), the test shown, where the pointer
16
+ * was released and the canvas to anchor a menu on. The wafers' own dies are the population.
17
+ */
18
+ onSelectWafers?: (sel: {
19
+ keys: number[];
20
+ testNumber: number;
21
+ }, at: {
22
+ x: number;
23
+ y: number;
24
+ }, anchor: HTMLElement) => void;
14
25
  /**
15
26
  * What the dashed reference line is, as a noun phrase — "lot mean" when the
16
27
  * items are one lot, else "mean of all wafers" (see stats/population.ts).
@@ -1 +1 @@
1
- import{buildTestTrendData as ke,trendCentre as we}from"../../stats/trend.js";import{isParametricTest as Ce}from"../../renderer/buildWaferMap.js";import{fmt as W}from"../../renderer/fmt.js";import{SPACE as re,fontPx as ce,FONT as Ae,CLR as Me}from"../toolbar.js";import{QUANTITY as G}from"./palette.js";import{cardShell as Pe,observeResize as Se,makeTooltip as Ne,positionChartTooltip as Ee,makeLinkedTestSelect as Ie,makeLinkedAxisPrefs as De,renderEmptyState as U,chartFillHeight as Oe,PADDING as j,resolveAxisRange as We,shouldIncludeLimitsByDefault as He,drawOffAxisLimits as $e,limitLines as me,limitExtent as de,hasBothLimitKinds as Re,stackLabelRows as Be,strokeLimitLine as Fe,makeAxisFormat as _e,VERTICAL_TICK_SPACING_PX as Xe,WAFER_MENU_HINT as ze,prepareCanvas as Ge}from"./chartShell.js";import{fitTicks as Ue}from"../../renderer/axisTicks.js";import{escHtml as K}from"../../core/utils.js";import{maxOf as je}from"../../core/utils.js";const fe=220,Ke=56,Ye=34,Qe=3.5;export function renderTrendPanel(d){const{items:Y,testDefs:ue,onSaveImage:he,onOpen:C,centreLabel:Q="mean of all wafers"}=d,pe=d.title??"Wafer-to-wafer trend",{card:a,body:u,controlsRow:V}=Pe(pe,he,d.ownerDocument);a.style.alignSelf="start";const N=ue.filter(s=>s.testNumber!==void 0&&Ce(s));let h=d.selectedTestNumber??N[0]?.testNumber??null,H=0;const q=Ie(N,h,s=>{h=s,A(),d.onTestChange?.(s)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument});V.appendChild(q.el);const $=a.ownerDocument.createElement("span");Object.assign($.style,{display:"inline-flex",gap:re.lg,alignItems:"center"}),V.appendChild($);const R=De($,d.axisPrefs,s=>{d.onAxisPrefsChange?.(s),A()},a.ownerDocument),w=a.ownerDocument.createElement("div");Object.assign(w.style,{color:Me.label,fontSize:Ae.body,marginBottom:re.sm}),a.insertBefore(w,u);const b=Ne(a);let E=null,i=[];function xe(s){return N.find(B=>B.testNumber===s)}function A(){const{includeLimits:s,clipOutliers:B,limits:be}=R.get();if(u.innerHTML="",E?.disconnect(),E=null,N.length===0||h===null){w.textContent="",U(u,"No parametric test data available for a trend.");return}if(Y.length<2){w.textContent="",U(u,"A wafer-to-wafer trend needs at least two wafers.");return}if(i=ke(Y,h),!i.some(o=>o.count>0)){w.textContent="",U(u,"No values recorded for this test.");return}const p=xe(h),I=me(p,be),g=we(i),J=()=>{w.textContent=`Point = wafer mean, whisker = \xB11\u03C3, dashed = ${Q} \xB7 wafers in slot order`+(C?" \xB7 click a point to open that wafer":"")+(H?` \xB7 axis clipped, ${H} outside`:"")};J();const m=a.ownerDocument.createElement("canvas");m.style.display="block",u.appendChild(m);let M=null;const Z=()=>{const o=Math.max(1,u.clientWidth),r=Math.max(fe,Oe(a,u,m,fe)),n=Ge(m,a,o,r);if(!n)return;const{ctx:e,theme:y}=n;let P=1/0,S=-1/0;const ee=[];for(const t of i)!t.count||!Number.isFinite(t.mean)||(P=Math.min(P,t.mean-t.stddev),S=Math.max(S,t.mean+t.stddev),ee.push(t.mean-t.stddev,t.mean+t.stddev));if(!Number.isFinite(P)||!Number.isFinite(S))return;const te=s??He(P,S,de(I).lo,de(I).hi);R.sync(te,me(p).length>0,Re(p));const D=We({dataMin:P,dataMax:S,limits:I,includeLimits:te,clipOutliers:B,values:ee});H=D.clippedCount,J();let x=D.lo,v=D.hi;const ne=(v-x)*.08;x-=ne,v+=ne;const c=j+Ke;e.font=`${ce(-1)}px system-ui, sans-serif`;const ie=t=>t.length>8?`${t.slice(0,7)}\u2026`:t,ge=i.length?Math.ceil(Math.max(e.measureText(ie(i[0].label)).width,e.measureText(ie(i[i.length-1].label)).width)/2):0,T=o-j-ge,O=j,L=r-Ye,k=t=>L-(t-x)/(v-x)*(L-O),se=i.length>1?(T-c)/(i.length-1):0;M={left:c,step:se};const _=t=>i.length>1?c+t*se:(c+T)/2;e.strokeStyle=y.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,O),e.lineTo(c,L),e.lineTo(T,L),e.stroke(),e.fillStyle=y.text,e.textAlign="right",e.textBaseline="middle";const oe=Ue(x,v,L-O,()=>Xe),ye=_e(Math.max(Math.abs(x),Math.abs(v)),p?.unit,oe.step||void 0);for(const t of oe.ticks)e.fillText(ye.tickWithUnit(t),c-6,k(t));$e(e,D.offAxis,{left:c,right:T,top:O,bottom:L},"vertical",y.limitLine,t=>W(t,p?.unit));const le=ce(-1),X=I.filter(t=>t.value>=x&&t.value<=v).map(t=>{const l=k(t.value);return{l:t,y:l,start:l-6-le/2,end:l-6+le/2}}),ve=Be(X,1),Te=je(X.map(t=>e.measureText(t.l.label).width))+8;if(X.forEach(({l:t,y:l},f)=>{e.save(),e.globalAlpha=.7,Fe(e,t,y.limitLine,c,l,T,l),e.restore(),e.save(),e.fillStyle=y.limitLine,e.textAlign="left",e.fillText(t.label,c+3+ve[f]*Te,l-6),e.restore()}),g!==null&&g>=x&&g<=v){const t=k(g);e.save(),e.strokeStyle=y.text,e.globalAlpha=.5,e.setLineDash([3,3]),e.beginPath(),e.moveTo(c,t),e.lineTo(T,t),e.stroke(),e.restore()}e.strokeStyle=G,e.lineWidth=1.5,e.beginPath();let z=!1;i.forEach((t,l)=>{if(!t.count||!Number.isFinite(t.mean)){z=!1;return}const f=_(l),ae=k(t.mean);z?e.lineTo(f,ae):e.moveTo(f,ae),z=!0}),e.stroke(),i.forEach((t,l)=>{if(!t.count||!Number.isFinite(t.mean))return;const f=_(l);t.stddev>0&&(e.save(),e.strokeStyle=G,e.globalAlpha=.55,e.lineWidth=1,e.beginPath(),e.moveTo(f,k(t.mean-t.stddev)),e.lineTo(f,k(t.mean+t.stddev)),e.stroke(),e.restore()),e.fillStyle=G,e.beginPath(),e.arc(f,k(t.mean),Qe,0,Math.PI*2),e.fill()}),e.fillStyle=y.text,e.textBaseline="top",e.textAlign="center";const Le=Math.max(1,Math.ceil(i.length*42/Math.max(1,T-c)));i.forEach((t,l)=>{if(l%Le!==0&&l!==i.length-1)return;const f=t.label.length>8?`${t.label.slice(0,7)}\u2026`:t.label;e.fillText(f,_(l),L+6)})},F=o=>{if(!M||M.step<=0)return-1;const r=m.getBoundingClientRect(),n=Math.round((o.clientX-r.left-M.left)/M.step);return n>=0&&n<i.length&&i[n].count>0?n:-1};m.addEventListener("mousemove",o=>{const r=F(o);if(r<0){b.style.display="none",m.style.cursor="default";return}const n=i[r];m.style.cursor=C&&n.key!==void 0?"pointer":"default",b.innerHTML=`<strong>${K(n.label)}</strong><br>`+K(`mean ${W(n.mean,p?.unit)} \xB7 \u03C3 ${W(n.stddev,p?.unit)}`)+`<br>n = ${n.count.toLocaleString()}`+(g!==null?"<br>"+K(`\u0394 vs ${Q} ${n.mean-g>=0?"+":""}${W(n.mean-g,p?.unit)}`):"")+(C&&n.key!==void 0?"<br><em>click to open this wafer</em>":"")+(d.onWaferContextMenu&&n.key!==void 0?ze:""),b.style.display="block",Ee(b,a,o.clientX,o.clientY)}),m.addEventListener("mouseleave",()=>{b.style.display="none"}),m.addEventListener("click",o=>{const r=F(o);if(r<0||!C)return;const n=i[r];n.key!==void 0&&h!==null&&C(n.key,h)}),m.addEventListener("contextmenu",o=>{const r=F(o),n=r<0?void 0:i[r].key;n===void 0||!d.onWaferContextMenu||(b.style.display="none",d.onWaferContextMenu(n,h??void 0,o))}),Z(),E=Se(u,Z)}return A(),{card:a,destroy:()=>{E?.disconnect(),b.remove(),a.remove()},setTest:s=>{q.set(s)&&(h=s,A())},setAxisPrefs:s=>{R.set(s)&&A()}}}
1
+ import{buildTestTrendData as Se,trendCentre as Ce}from"../../stats/trend.js";import{isParametricTest as Ae}from"../../renderer/buildWaferMap.js";import{fmt as H}from"../../renderer/fmt.js";import{SPACE as ce,fontPx as me,FONT as Pe,CLR as Me}from"../toolbar.js";import{QUANTITY as X}from"./palette.js";import{wirePointInteractions as Ne}from"./pointInteractions.js";import{cardShell as Ie,observeResize as We,makeTooltip as Ee,makeLinkedTestSelect as De,makeLinkedAxisPrefs as Oe,renderEmptyState as j,chartFillHeight as He,PADDING as K,resolveAxisRange as $e,shouldIncludeLimitsByDefault as Re,drawOffAxisLimits as _e,limitLines as fe,limitExtent as de,hasBothLimitKinds as Be,stackLabelRows as Fe,strokeLimitLine as ze,makeAxisFormat as Ge,VERTICAL_TICK_SPACING_PX as Ue,WAFER_MENU_HINT as Xe,prepareCanvas as je}from"./chartShell.js";import{fitTicks as Ke}from"../../renderer/axisTicks.js";import{escHtml as Q}from"../../core/utils.js";import{maxOf as Qe}from"../../core/utils.js";const ue=220,Ve=56,Ye=34,he=3.5;export function renderTrendPanel(a){const{items:V,testDefs:pe,onSaveImage:xe,onOpen:M,centreLabel:Y="mean of all wafers"}=a,be=a.title??"Wafer-to-wafer trend",{card:c,body:h,controlsRow:q}=Ie(be,xe,a.ownerDocument);c.style.alignSelf="start";const N=pe.filter(l=>l.testNumber!==void 0&&Ae(l));let f=a.selectedTestNumber??N[0]?.testNumber??null,$=0;const J=De(N,f,l=>{f=l,S(),a.onTestChange?.(l)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:c.ownerDocument});q.appendChild(J.el);const R=c.ownerDocument.createElement("span");Object.assign(R.style,{display:"inline-flex",gap:ce.lg,alignItems:"center"}),q.appendChild(R);const _=Oe(R,a.axisPrefs,l=>{a.onAxisPrefsChange?.(l),S()},c.ownerDocument),y=c.ownerDocument.createElement("div");y.dataset.wmapCaption="1",Object.assign(y.style,{color:Me.label,fontSize:Pe.body,marginBottom:ce.sm}),c.insertBefore(y,h);const B=Ee(c);let I=null,s=[];function ge(l){return N.find(F=>F.testNumber===l)}function S(){const{includeLimits:l,clipOutliers:F,limits:ye}=_.get();if(h.innerHTML="",I?.disconnect(),I=null,N.length===0||f===null){y.textContent="",j(h,"No parametric test data available for a trend.");return}if(V.length<2){y.textContent="",j(h,"A wafer-to-wafer trend needs at least two wafers.");return}if(s=Se(V,f),!s.some(i=>i.count>0)){y.textContent="",j(h,"No values recorded for this test.");return}const p=ge(f),W=fe(p,ye),v=Ce(s),Z=()=>{y.textContent=`Point = wafer mean, whisker = \xB11\u03C3, dashed = ${Y} \xB7 wafers in slot order`+(M?" \xB7 click a point to open that wafer":"")+(a.onSelectWafers?" \xB7 drag across the plot to select wafers":"")+($?` \xB7 axis clipped, ${$} outside`:"")};Z();const x=c.ownerDocument.createElement("canvas");x.style.display="block",h.appendChild(x);let T=null;const C=new Set,E=()=>{const i=Math.max(1,h.clientWidth),n=Math.max(ue,He(c,h,x,ue)),o=je(x,c,i,n);if(!o)return;const{ctx:e,theme:u}=o;let A=1/0,P=-1/0;const te=[];for(const t of s)!t.count||!Number.isFinite(t.mean)||(A=Math.min(A,t.mean-t.stddev),P=Math.max(P,t.mean+t.stddev),te.push(t.mean-t.stddev,t.mean+t.stddev));if(!Number.isFinite(A)||!Number.isFinite(P))return;const ne=l??Re(A,P,de(W).lo,de(W).hi);_.sync(ne,fe(p).length>0,Be(p));const D=$e({dataMin:A,dataMax:P,limits:W,includeLimits:ne,clipOutliers:F,values:te});$=D.clippedCount,Z();let b=D.lo,k=D.hi;const se=(k-b)*.08;b-=se,k+=se;const m=K+Ve;e.font=`${me(-1)}px system-ui, sans-serif`;const ie=t=>t.length>8?`${t.slice(0,7)}\u2026`:t,ve=s.length?Math.ceil(Math.max(e.measureText(ie(s[0].label)).width,e.measureText(ie(s[s.length-1].label)).width)/2):0,w=i-K-ve,O=K,L=n-Ye,g=t=>L-(t-b)/(k-b)*(L-O),oe=s.length>1?(w-m)/(s.length-1):0;T={left:m,step:oe};const z=t=>s.length>1?m+t*oe:(m+w)/2;e.strokeStyle=u.border,e.lineWidth=1,e.beginPath(),e.moveTo(m,O),e.lineTo(m,L),e.lineTo(w,L),e.stroke(),e.fillStyle=u.text,e.textAlign="right",e.textBaseline="middle";const le=Ke(b,k,L-O,()=>Ue),Te=Ge(Math.max(Math.abs(b),Math.abs(k)),p?.unit,le.step||void 0);for(const t of le.ticks)e.fillText(Te.tickWithUnit(t),m-6,g(t));_e(e,D.offAxis,{left:m,right:w,top:O,bottom:L},"vertical",u.limitLine,t=>H(t,p?.unit));const re=me(-1),G=W.filter(t=>t.value>=b&&t.value<=k).map(t=>{const r=g(t.value);return{l:t,y:r,start:r-6-re/2,end:r-6+re/2}}),ke=Fe(G,1),we=Qe(G.map(t=>e.measureText(t.l.label).width))+8;if(G.forEach(({l:t,y:r},d)=>{e.save(),e.globalAlpha=.7,ze(e,t,u.limitLine,m,r,w,r),e.restore(),e.save(),e.fillStyle=u.limitLine,e.textAlign="left",e.fillText(t.label,m+3+ke[d]*we,r-6),e.restore()}),v!==null&&v>=b&&v<=k){const t=g(v);e.save(),e.strokeStyle=u.text,e.globalAlpha=.5,e.setLineDash([3,3]),e.beginPath(),e.moveTo(m,t),e.lineTo(w,t),e.stroke(),e.restore()}e.strokeStyle=X,e.lineWidth=1.5,e.beginPath();let U=!1;s.forEach((t,r)=>{if(!t.count||!Number.isFinite(t.mean)){U=!1;return}const d=z(r),ae=g(t.mean);U?e.lineTo(d,ae):e.moveTo(d,ae),U=!0}),e.stroke(),s.forEach((t,r)=>{if(!t.count||!Number.isFinite(t.mean))return;const d=z(r);t.stddev>0&&(e.save(),e.strokeStyle=X,e.globalAlpha=.55,e.lineWidth=1,e.beginPath(),e.moveTo(d,g(t.mean-t.stddev)),e.lineTo(d,g(t.mean+t.stddev)),e.stroke(),e.restore()),e.fillStyle=X,e.beginPath(),e.arc(d,g(t.mean),he,0,Math.PI*2),e.fill(),C.has(r)&&(e.beginPath(),e.arc(d,g(t.mean),he+3,0,Math.PI*2),e.strokeStyle=u.text,e.lineWidth=1.5,e.stroke())}),e.fillStyle=u.text,e.textBaseline="top",e.textAlign="center";const Le=Math.max(1,Math.ceil(s.length*42/Math.max(1,w-m)));s.forEach((t,r)=>{if(r%Le!==0&&r!==s.length-1)return;const d=t.label.length>8?`${t.label.slice(0,7)}\u2026`:t.label;e.fillText(d,z(r),L+6)})},ee=i=>{if(!T||T.step<=0)return-1;const n=x.getBoundingClientRect(),o=Math.round((i.clientX-n.left-T.left)/T.step);return o>=0&&o<s.length&&s[o].count>0?o:-1};Ne({card:c,canvas:x,tooltip:B,drawn:()=>[],locate:i=>{const n=ee(i);return n<0?null:n},describe:i=>{const n=s[i];return`<strong>${Q(n.label)}</strong><br>`+Q(`mean ${H(n.mean,p?.unit)} \xB7 \u03C3 ${H(n.stddev,p?.unit)}`)+`<br>n = ${n.count.toLocaleString()}`+(v!==null?"<br>"+Q(`\u0394 vs ${Y} ${n.mean-v>=0?"+":""}${H(n.mean-v,p?.unit)}`):"")+(M&&n.key!==void 0?"<br><em>click to open this wafer</em>":"")+(a.onSelectWafers?"<br><em>drag across the plot to select wafers</em>":"")+(a.onWaferContextMenu&&n.key!==void 0?Xe:"")},clickable:i=>!!M&&s[i].key!==void 0,onOpen:i=>{const n=s[i];n.key!==void 0&&f!==null&&M(n.key,f)},onHover:()=>{},select:a.onSelectWafers?{pick:({x0:i,x1:n})=>{if(!T)return[];const o=T;return s.map((e,u)=>u).filter(e=>s[e].count>0&&o.left+e*o.step>=i&&o.left+e*o.step<=n)},onPicked:(i,n)=>{C.clear();for(const e of i)C.add(e);E();const o=i.map(e=>s[e].key).filter(e=>e!==void 0);o.length>0&&f!==null&&a.onSelectWafers({keys:o,testNumber:f},n,x)},onClear:()=>{C.clear(),E()},hasSelection:()=>C.size>0}:void 0}),x.addEventListener("contextmenu",i=>{const n=ee(i),o=n<0?void 0:s[n].key;o===void 0||!a.onWaferContextMenu||(B.style.display="none",a.onWaferContextMenu(o,f??void 0,i))}),E(),I=We(h,E)}return S(),{card:c,destroy:()=>{I?.disconnect(),B.remove(),c.remove()},setTest:l=>{J.set(l)&&(f=l,S())},setAxisPrefs:l=>{_.set(l)&&S()}}}
@@ -0,0 +1,13 @@
1
+ export interface ConfirmOptions {
2
+ doc: Document;
3
+ /** An element from the live DOM the dialog belongs to, so it lands inside a host's own `<dialog>` (see `OverlayOptions.anchor`). */
4
+ anchor: Element;
5
+ title: string;
6
+ /** What will happen, in a sentence or two. Plain text. */
7
+ message: string;
8
+ /** The destructive button's label, naming the action ("Delete 4 plots"), not "OK". */
9
+ confirmLabel: string;
10
+ cancelLabel?: string;
11
+ }
12
+ export declare function confirmDialog(o: ConfirmOptions): Promise<boolean>;
13
+ //# sourceMappingURL=confirmDialog.d.ts.map
@@ -0,0 +1 @@
1
+ import{openModal as u,controlStyle as y,wireControlHover as C,CLR as w,FONT as m,SPACE as i}from"./toolbar.js";export function confirmDialog(t){const{doc:o}=t;return new Promise(p=>{let s=!1,l;const n=u({title:t.title,anchor:t.anchor,ownerDocument:o,boxSize:{width:"min(90vw, 460px)",height:"auto"},maximizable:!1,initialFocus:()=>l,onClose:()=>p(s)});n.box.dataset.wmapConfirm="1",n.box.style.minHeight="0";const c=o.createElement("div");Object.assign(c.style,{display:"flex",flexDirection:"column",gap:i.lg,padding:i.xl,color:w.text,fontSize:m.body});const a=o.createElement("p");a.textContent=t.message,a.id=`wmap-confirm-${Math.random().toString(36).slice(2,8)}`,a.style.margin="0",n.box.setAttribute("aria-describedby",a.id);const r=o.createElement("div");Object.assign(r.style,{display:"flex",justifyContent:"flex-end",gap:i.md,flexWrap:"wrap"});const d=(f,g,x)=>{const e=o.createElement("button");return e.type="button",e.textContent=f,e.dataset[g]="1",Object.assign(e.style,{...y("outlined"),fontSize:m.body,padding:`${i.sm} ${i.lg}`}),C(e),e.addEventListener("click",x),e};l=d(t.cancelLabel??"Cancel","wmapConfirmCancel",()=>n.close());const b=d(t.confirmLabel,"wmapConfirmOk",()=>{s=!0,n.close()});r.append(l,b),c.append(a,r),n.contentWrap.appendChild(c)})}