@wafertools/wafermap 0.30.1 → 0.30.3
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.
- package/AGENTS.md +71 -11
- package/CHANGELOG.md +464 -8
- package/README.md +6 -3
- package/dist/packages/canvas-adapter/chartPopulation.d.ts +48 -0
- package/dist/packages/canvas-adapter/chartPopulation.js +1 -0
- package/dist/packages/canvas-adapter/charts/barPanel.d.ts +4 -1
- package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
- package/dist/packages/canvas-adapter/charts/boxplot.d.ts +4 -1
- package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
- package/dist/packages/canvas-adapter/charts/capability.js +1 -1
- package/dist/packages/canvas-adapter/charts/chartShell.d.ts +81 -7
- package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
- package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
- package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
- package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
- package/dist/packages/canvas-adapter/charts/sweep.d.ts +31 -0
- package/dist/packages/canvas-adapter/charts/sweep.js +1 -0
- package/dist/packages/canvas-adapter/charts/trend.d.ts +3 -1
- package/dist/packages/canvas-adapter/charts/trend.js +1 -1
- package/dist/packages/canvas-adapter/chunked.d.ts +45 -0
- package/dist/packages/canvas-adapter/chunked.js +1 -0
- package/dist/packages/canvas-adapter/dieList.js +2 -2
- package/dist/packages/canvas-adapter/drilldown.d.ts +23 -0
- package/dist/packages/canvas-adapter/drilldown.js +1 -0
- package/dist/packages/canvas-adapter/icons.js +1 -1
- package/dist/packages/canvas-adapter/insightsTab.d.ts +55 -3
- package/dist/packages/canvas-adapter/insightsTab.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +55 -0
- package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +55 -9
- package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
- package/dist/packages/canvas-adapter/toCanvas.js +1 -1
- package/dist/packages/canvas-adapter/toolbar.d.ts +30 -2
- package/dist/packages/canvas-adapter/toolbar.js +2 -2
- package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
- package/dist/packages/canvas-adapter/userGuideHtml.js +126 -3
- package/dist/packages/canvas-adapter/version.d.ts +2 -2
- package/dist/packages/canvas-adapter/version.js +1 -1
- package/dist/packages/canvas-adapter/warnings.js +1 -1
- package/dist/packages/core/aggregates.js +1 -1
- package/dist/packages/core/utils.d.ts +55 -0
- package/dist/packages/core/utils.js +1 -1
- package/dist/packages/renderer/axisTicks.d.ts +32 -0
- package/dist/packages/renderer/axisTicks.js +1 -0
- package/dist/packages/renderer/binColors.d.ts +14 -0
- package/dist/packages/renderer/binColors.js +1 -1
- package/dist/packages/renderer/buildView.d.ts +11 -37
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.d.ts +58 -1
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/renderer/deprecate.d.ts +13 -5
- package/dist/packages/renderer/deprecate.js +1 -1
- package/dist/packages/renderer/derivedTests/apply.d.ts +85 -0
- package/dist/packages/renderer/derivedTests/apply.js +1 -0
- package/dist/packages/renderer/derivedTests/evaluate.d.ts +48 -0
- package/dist/packages/renderer/derivedTests/evaluate.js +1 -0
- package/dist/packages/renderer/derivedTests/parser.d.ts +148 -0
- package/dist/packages/renderer/derivedTests/parser.js +2 -0
- package/dist/packages/renderer/fmt.d.ts +24 -2
- package/dist/packages/renderer/fmt.js +1 -1
- package/dist/packages/renderer/index.d.ts +1 -0
- package/dist/packages/renderer/index.js +1 -1
- package/dist/packages/renderer/spec.d.ts +49 -0
- package/dist/packages/renderer/spec.js +1 -0
- package/dist/packages/renderer/testLabel.d.ts +154 -0
- package/dist/packages/renderer/testLabel.js +1 -0
- package/dist/packages/stats/analyzeWaferMap.js +1 -1
- package/dist/packages/stats/capability.d.ts +60 -0
- package/dist/packages/stats/capability.js +1 -1
- package/dist/packages/stats/correlation.d.ts +33 -0
- package/dist/packages/stats/correlation.js +1 -1
- package/dist/packages/stats/deprecated.d.ts +3 -0
- package/dist/packages/stats/deprecated.js +1 -1
- package/dist/packages/stats/findingsNarrative.js +1 -1
- package/dist/packages/stats/index.d.ts +2 -2
- package/dist/packages/stats/index.js +1 -1
- package/dist/packages/stats/math.d.ts +27 -2
- package/dist/packages/stats/math.js +1 -1
- package/dist/packages/stats/mergeTestDefs.js +1 -1
- package/dist/packages/stats/renderFindingsReport.js +8 -28
- package/dist/packages/stats/renderSummaryReport.js +22 -31
- package/dist/packages/stats/reportHtml.d.ts +27 -0
- package/dist/packages/stats/reportHtml.js +35 -14
- package/dist/packages/stats/sweep.d.ts +178 -0
- package/dist/packages/stats/sweep.js +1 -0
- package/dist/packages/stats/sweepXFromName.d.ts +27 -0
- package/dist/packages/stats/sweepXFromName.js +1 -0
- package/dist/packages/stats/testPassRate.js +1 -1
- package/dist/packages/stats/types.d.ts +28 -1
- package/llms.txt +14 -8
- package/package.json +2 -2
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{buildCorrelationMatrix as
|
|
2
|
-
`),"test-correlation.csv","text/csv",
|
|
1
|
+
import{buildCorrelationMatrix as vt,filterCorrelationMatrix as Lt,correlationSampleNote as It}from"../../stats/correlation.js";import{csvField as K,maxOf as $t}from"../../core/utils.js";import{CORRELATION_POSITIVE as ct,CORRELATION_NEGATIVE as dt}from"./palette.js";import{buildFacetTable as St}from"../../stats/facets.js";import{wireControlHover as Tt,controlStyle as Dt,SPACE as v,RADIUS as st,fontPx as ut,FONT as j,CLR as b,saveTextFile as Rt}from"../toolbar.js";import{attachChartTip as At,cardShell as kt,setChartGrow as _t,isExpandedCard as Bt,bodyRoom as Nt,observeResize as Ot,makeTooltip as Wt,positionChartTooltip as Ht,makeWaferSelect as jt,renderEmptyState as Xt,resolveChartCanvasColors as Ft,prepareCanvas as zt,chartDpr as ft}from"./chartShell.js";import{escHtml as q}from"../../core/utils.js";import{unmarkedLabel as mt,derivedCsvCell as pt,DERIVED_CSV_HEADER as ht,DERIVED_MARK as Vt,DERIVED_KEY as Yt}from"../../renderer/testLabel.js";const xt=5,bt=100,gt=20;function lt(g){const d=g.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(d)return[+d[1],+d[2],+d[3]];const p=g.trim().replace("#","");return p.length===6?[parseInt(p.slice(0,2),16),parseInt(p.slice(2,4),16),parseInt(p.slice(4,6),16)]:null}function Pt(g){const d=lt(g);return d?(d[0]*299+d[1]*587+d[2]*114)/1e3>140?"#111":"#fff":"#000"}function Gt(g,d,p){const N=lt(g);if(!N)return g;const J=Math.round(d[0]+(N[0]-d[0])*p),O=Math.round(d[1]+(N[1]-d[1])*p),l=Math.round(d[2]+(N[2]-d[2])*p);return`rgb(${J},${O},${l})`}export function renderCorrelationPanel(g){const{title:d="Test correlation matrix",items:p,testDefs:N,onSaveImage:J,onSelectPair:O}=g,{card:l,body:L,controlsRow:Q}=kt(d,J,g.ownerDocument);_t(l,"square"),L.style.overflowX="auto",l.style.alignSelf="start";let V=gt,X=null;const F=l.ownerDocument.createElement("label");F.textContent="Max tests:",Object.assign(F.style,{color:b.label,fontSize:j.body,display:"flex",alignItems:"center",gap:v.xs});const w=l.ownerDocument.createElement("input");w.type="number",w.min=String(xt),w.max=String(bt),w.value=String(V),Object.assign(w.style,{width:"52px",fontSize:j.body,padding:`${v.xxs} ${v.xs}`,background:b.menuBg,color:b.value,border:`1px solid ${b.menuBorder}`,borderRadius:st.control}),w.addEventListener("change",()=>{const e=Math.max(xt,Math.min(bt,parseInt(w.value,10)||gt));w.value=String(e),e!==V&&(V=e,tt())}),F.appendChild(w),Q.appendChild(F),p.length>1&&Q.appendChild(jt(p,X,e=>{X=e,tt()},{ownerDocument:l.ownerDocument}));function yt(){return X!==null?p[X]?[p[X]]:[]:p}if(g.onSaveText){const e=l.ownerDocument.createElement("button");e.type="button",e.textContent="Correlation CSV",Object.assign(e.style,{...Dt("outlined"),color:b.text}),Tt(e),e.addEventListener("click",()=>{const o=rt;if(!o)return;const a=o.tests.some(f=>f.derived),u=["Test X,Test X number,Test Y,Test Y number,r,n"+(a?`,Test X ${ht.toLowerCase()},Test Y ${ht.toLowerCase()}`:"")];for(let f=0;f<o.tests.length;f++)for(let i=f+1;i<o.tests.length;i++){const y=o.cells.find(h=>h.xIndex===i&&h.yIndex===f);y&&u.push([K(mt(o.tests[i].label)),String(o.tests[i].testNumber),K(mt(o.tests[f].label)),String(o.tests[f].testNumber),y.r===null?"":y.r.toFixed(6),String(y.n),...a?[K(pt(o.tests[i])),K(pt(o.tests[f]))]:[]].join(","))}Rt(u.join(`
|
|
2
|
+
`),"test-correlation.csv","text/csv",g.onSaveText)}),Q.appendChild(e)}const k=l.ownerDocument.createElement("div");Object.assign(k.style,{display:"flex",flexDirection:"column",gap:v.xs,marginBottom:v.sm}),l.insertBefore(k,L);const I=Wt(l);At(F,l,I,"Cap on how many tests the matrix includes (strongest correlations kept first)");let Z=()=>{};function Et(e,o,a,u,f,i,y,h){k.innerHTML="";const _=It(y);if(_){const x=l.ownerDocument.createElement("div");x.textContent=`${_} \u2014 r is an estimate, not the whole population.`,Object.assign(x.style,{color:b.label,fontSize:j.body}),k.appendChild(x)}if(f.length>0){const x=l.ownerDocument.createElement("div");x.textContent=`\u26A0 Mixed ${f.join(", ")} within this set \u2014 correlations may be misleading (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,Object.assign(x.style,{color:b.warnText,background:b.warnBg,border:`1px solid ${b.warnBorder}`,borderRadius:st.control,padding:`${v.xs} ${v.md}`,fontSize:j.body}),k.appendChild(x)}const M=l.ownerDocument.createElement("span");Object.assign(M.style,{display:"inline-flex",alignItems:"center",gap:v.sm,color:b.label,fontSize:j.body,flexWrap:"wrap"});const W=l.ownerDocument.createElement("span");W.textContent=(i!==null?`Pearson r \xB7 n \u2248 ${i.toLocaleString()} dies per pair \xB7 click a cell to view that pair in scatter \xB7`:"Pearson r \xB7 click a cell to view that pair in scatter \xB7")+(h?` ${Vt} ${Yt} \xB7`:""),M.appendChild(W);const r=l.ownerDocument.createElement("span");Object.assign(r.style,{display:"inline-flex",alignItems:"center",gap:v.xs});const $=l.ownerDocument.createElement("span");$.textContent="\u22121";const Y=l.ownerDocument.createElement("span");Object.assign(Y.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:st.control,border:`1px solid ${b.menuBorder}`,background:`linear-gradient(to right, ${dt}, ${b.menuBg}, ${ct})`});const z=l.ownerDocument.createElement("span");z.textContent="+1",r.append($,Y,z),M.appendChild(r),k.appendChild(M);const S=l.ownerDocument.createElement("span");if(Object.assign(S.style,{color:b.value,fontSize:j.body,fontWeight:"500"}),e===0&&o===0)S.textContent=u?`No significant correlations found \u2014 strongest pair: ${u.xLabel} \u2194 ${u.yLabel} (r = ${u.r.toFixed(2)})`:"No significant correlations found";else{const x=[];e>0&&x.push(`${e} strong (|r| \u2265 0.7)`),o>0&&x.push(`${o} moderate (0.4\u20130.7)`);const P=e+o;let s=x.join(", ")+` pair${P!==1?"s":""} found`;a>0&&(s+=` \xB7 ${a} weak pair${a!==1?"s":""} not shown`),S.textContent=s}k.appendChild(S)}function Mt(e){if(L.innerHTML="",e.tests.length<2)return Xt(L,"Need at least two parametric tests for a correlation matrix."),()=>{};const o=l.ownerDocument.createElement("canvas");o.style.display="block",o.style.cursor="default",L.appendChild(o);const a=e.tests.length,u=s=>s.label.split(" (#")[0],f=Math.min(14,$t(e.tests.map(s=>u(s).length))),i=f*6.5+8,y=10,h=Math.round(y*6.5*Math.sin(Math.PI/4))+14,_=14,M=28,W=44;let r=-1,$=-1;const Y=96;function z(s){const c=Math.max(0,s-i),m=Math.floor(c/a);if(!Bt(l))return Math.max(_,Math.min(W,m));const n=Math.floor((Nt(l,L,o)-h-4)/a);return Math.max(_,Math.min(Y,m,n))}const S=new Map;for(const s of e.cells){let c=S.get(s.yIndex);c||(c=[],S.set(s.yIndex,c)),c.push(s)}function x(){const s=Ft(l),c=lt(s.bg)??[255,255,255],m=L.clientWidth,n=z(m),H=n*a,T=h+n*a+4,D=i+H,R=zt(o,l,D,T);if(!R)return;const{ctx:t}=R;t.font=`${ut(-1)}px system-ui, sans-serif`,t.fillStyle=s.text,e.tests.forEach((A,E)=>{const C=u(A),B=C.length>y?`${C.slice(0,y-1)}\u2026`:C,et=i+E*n+n/2;t.save(),t.translate(et,h-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=E===r?s.text:s.textMuted,t.fillText(B,0,0),t.restore()}),e.tests.forEach((A,E)=>{const C=u(A),B=h+E*n,et=B+n/2;t.font=`${ut(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle",t.fillStyle=E===$?s.text:s.textMuted,t.fillText(C.length>f?`${C.slice(0,f-1)}\u2026`:C,i-4,et),(S.get(E)??[]).forEach(it=>{const nt=it.xIndex,G=i+nt*n,wt=nt===r&&E===$,at=nt===E,U=it.r;if(U===null){t.fillStyle=s.border,t.globalAlpha=.4,t.fillRect(G+1,B+1,n-2,n-2),t.globalAlpha=1;return}let ot=s.bgHover;at?t.fillStyle=s.bgHover:(ot=Gt(U>=0?ct:dt,c,Math.abs(U)),t.fillStyle=ot),t.fillRect(G+1,B+1,n-2,n-2),n>=M&&!at&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Pt(ot),t.textAlign="center",t.textBaseline="middle",t.fillText(U.toFixed(2),G+n/2,B+n/2)),wt&&(t.strokeStyle=s.text,t.lineWidth=2,t.strokeRect(G+1,B+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=s.bg,t.lineWidth=1;for(let A=0;A<=a;A++){const E=i+A*n;t.beginPath(),t.moveTo(E,h),t.lineTo(E,h+a*n),t.stroke();const C=h+A*n;t.beginPath(),t.moveTo(i,C),t.lineTo(i+a*n,C),t.stroke()}}function P(s){const c=o.getBoundingClientRect(),m=L.clientWidth,n=z(m),H=(s.clientX-c.left)*(o.width/ft(o)/c.width),T=(s.clientY-c.top)*(o.height/ft(o)/c.height),D=Math.floor((H-i)/n),R=Math.floor((T-h)/n);return D>=0&&D<a&&R>=0&&R<a?{xi:D,yi:R}:null}return o.addEventListener("mousemove",s=>{const c=P(s);if(!c){I.style.display="none",o.style.cursor="default";return}const{xi:m,yi:n}=c,H=m===n;o.style.cursor=H||!O?"default":"pointer";const T=e.cells.find(t=>t.xIndex===m&&t.yIndex===n),D=e.tests[m].label,R=e.tests[n].label;H?I.innerHTML=`<strong>${q(D)}</strong>`:T?.r!==null&&T?.r!==void 0?I.innerHTML=`<strong>${q(u(e.tests[n]))}</strong> (#${e.tests[n].testNumber}) vs <strong>${q(u(e.tests[m]))}</strong> (#${e.tests[m].testNumber})<br>r = ${T.r.toFixed(4)} \xB7 n = ${T.n.toLocaleString()}${O?"<br><em>click to view in scatter</em>":""}`:I.innerHTML=`${q(`${R} vs ${D}`)}<br><em>insufficient data</em>`,I.style.display="block",Ht(I,l,s.clientX,s.clientY)}),o.addEventListener("mouseleave",()=>{I.style.display="none"}),o.addEventListener("click",s=>{const c=P(s);if(!c||!O)return;const{xi:m,yi:n}=c;m!==n&&(r=m,$=n,x(),O(e.tests[m].testNumber,e.tests[n].testNumber))}),x}let rt=null;function tt(){const e=yt(),o=e.flatMap(r=>r.dies??[]),a=vt(o,N),{matrix:u,strongPairs:f,moderatePairs:i,hiddenWeakPairs:y,strongestPair:h}=Lt(a,{minTests:6,maxTests:V}),_=St(e,{facetableOnly:!0}).filter(r=>r.splittable).map(r=>r.label),M=u.cells.filter(r=>r.xIndex!==r.yIndex).map(r=>r.n).sort((r,$)=>r-$),W=M.length?M[Math.floor(M.length/2)]:null;rt=u,Et(f,i,y,h,_,W,a.sample,u.tests.some(r=>r.derived)),Z=Mt(u),Z()}const Ct=Ot(l,()=>Z());return tt(),{card:l,destroy:()=>Ct.disconnect()}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{categorical as Y}from"./palette.js";import{SPACE as E,FONT as q,CLR as J}from"../toolbar.js";import{observeResize as K,positionChartTooltip as Q,
|
|
1
|
+
import{categorical as Y}from"./palette.js";import{SPACE as E,FONT as q,CLR as J}from"../toolbar.js";import{observeResize as K,positionChartTooltip as Q,fitRowsHeight as Z,setChartGrow as tt,prepareCanvas as et,chartSwatchCss as nt,PADDING as c,VALUE_WIDTH as D}from"./chartShell.js";const lt=8,w=1,a=14,ot=8;export function renderGroupedBarPlot(h,C,p,U){const{rows:v,groups:L,labelWidth:T,tooltipHtml:$,maxVisibleRows:j=ot,showLegend:k=!0,maxLabelChars:H,emptyBarText:R,colorOf:B=Y}=U,f=h.ownerDocument;tt(h,"rows");const A=Math.max(1,L.length),M=A*a+(A-1)*w,u=M+lt;let W=0;if(k){const e=f.createElement("div");Object.assign(e.style,{display:"flex",flexWrap:"wrap",gap:`${E.xs} ${E.xl}`,marginBottom:E.xs}),L.forEach((l,n)=>{const t=f.createElement("span");Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:E.sm,fontSize:q.body,color:J.text});const o=f.createElement("span");o.style.cssText=nt(B(n));const i=f.createElement("span");i.textContent=l,t.append(o,i),e.appendChild(t)}),C.appendChild(e),W=e.offsetHeight}const d=f.createElement("div"),G=c*2+Math.min(v.length,j)*u;Object.assign(d.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${G}px`,scrollbarGutter:"stable"}),C.appendChild(d);const s=f.createElement("canvas");s.style.display="block",s.style.cursor="default",d.appendChild(s);let r=null;function _(){const e=c+T,l=Math.max(10,s.clientWidth-e-D-c);return{barX:e,barMaxWidth:l}}function z(e,l){const n=Math.floor((l-c)/u);if(n<0||n>=v.length)return null;const t=l-c-n*u,o=Math.floor(t/(a+w));if(o<0||o>=A)return null;const{barX:i,barMaxWidth:m}=_();return e<i||e>i+m?null:{row:n,group:o}}function g(){const e=c*2+v.length*u;d.style.maxHeight=`${Z(h,C,G,e,W)}px`;const l=d.clientWidth,n=et(s,h,l,e);if(!n)return;const{ctx:t,theme:o}=n,{barX:i,barMaxWidth:m}=_();v.forEach((x,P)=>{const b=c+P*u;if(r&&r.row===P){const y=b+r.group*(a+w);t.fillStyle=o.bgHover,t.fillRect(0,y-1,l,a+2)}t.fillStyle=o.text,t.textAlign="right";const F=H!==void 0&&x.label.length>H?`${x.label.slice(0,H-1)}\u2026`:x.label;t.fillText(F,c+T-8,b+M/2),x.bars.forEach((y,X)=>{const S=b+X*(a+w);if(t.fillStyle=o.track,t.fillRect(i,S,m,a),y.fraction===null){R&&(t.fillStyle=o.textMuted,t.textAlign="left",t.fillText(R,i+4,S+a/2));return}const N=Math.max(y.fraction>0?1:0,y.fraction*m);t.fillStyle=B(X),t.fillRect(i,S,N,a)}),t.fillStyle=o.text,t.textAlign="right",t.fillText(x.trailing,i+m+D,b+M/2)})}const I=e=>{const l=s.getBoundingClientRect(),n=z(e.clientX-l.left,e.clientY-l.top),t=n?.row!==r?.row||n?.group!==r?.group;if(r=n,t&&g(),!n){p.style.display="none";return}p.innerHTML=$(n.row,n.group),p.style.display="block",Q(p,h,e.clientX,e.clientY)},O=()=>{r&&(r=null,g()),p.style.display="none"};s.addEventListener("mousemove",I),s.addEventListener("mouseleave",O);const V=K(h,()=>g());return g(),{draw:g,destroy:()=>{V?.disconnect(),s.removeEventListener("mousemove",I),s.removeEventListener("mouseleave",O)}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildTestHistogramData as _t,collectTestValues as At,buildTestHistogramSeries as Ut,testValueExtent as Ct}from"../../stats/histogram.js";import{SPACE as at,fontPx as Q,FONT as $t,CLR as Pt}from"../toolbar.js";import{fmt as bt}from"../../renderer/fmt.js";import{QUANTITY as Xt,categorical as Yt}from"./palette.js";import{cardShell as Qt,observeResize as Wt,makeTooltip as qt,attachChartTip as Jt,positionChartTooltip as Dt,makeLinkedTestSelect as Zt,makeWaferSelect as te,makeLinkedAxisPrefs as ee,renderEmptyState as Tt,chartFillHeight as Et,applyCanvasFlow as It,makeAxisFormat as Bt,PADDING as ft,robustFence as Ft,shouldIncludeLimitsByDefault as Kt,drawOffAxisLimits as ne,resolveAxisRange as ie,chartSwatchCss as oe,makeSeriesLegendItem as le,prepareCanvas as Ot}from"./chartShell.js";import{escHtml as St}from"../../core/utils.js";const wt=230,Rt=36,yt=18;function Nt(l,G,q,J,Mt,B,r,y=4){const Z=Math.max(1,B/y),rt=Math.pow(10,Math.floor(Math.log10(Z))),d=Z/rt,tt=(d<=1?1:d<=2?2:d<=5?5:10)*rt,K=J-q;l.save(),l.font=`${Q(-1)}px system-ui, sans-serif`,l.textAlign="right",l.textBaseline="middle";for(let V=0;V<=B+1e-9;V+=tt){const ct=J-V/(B||1)*K;l.strokeStyle=r.grid,l.lineWidth=1,l.beginPath(),l.moveTo(G,ct),l.lineTo(G+Mt,ct),l.stroke(),l.fillStyle=r.text,l.fillText(String(Math.round(V)),G-4,ct)}l.strokeStyle=r.axis,l.lineWidth=1,l.beginPath(),l.moveTo(G,q),l.lineTo(G,J),l.stroke(),l.restore()}export function renderHistogramPanel(l){const{title:G="Test value distribution",items:q,testDefs:J,onSaveImage:Mt,groups:B}=l,{card:r,body:y,controlsRow:Z}=Qt(G,Mt,l.ownerDocument),rt=J.filter(s=>s.testNumber!==void 0);let d=l.selectedTestNumber??rt[0]?.testNumber??null,tt=null,K=0;const V=Zt(rt,d,s=>{d=s,j(),l.onTestChange?.(s)},{maxWidth:"200px",ownerDocument:r.ownerDocument}),ct=V.el;Z.appendChild(ct);const vt=te(q,tt,s=>{tt=s,j()},{ownerDocument:r.ownerDocument});Z.appendChild(vt);function zt(s){return S===s||s!==null&&!(B??[]).some(v=>v.key===s)?!1:(S=s,j(),!0)}const Lt=r.ownerDocument.createElement("span");Object.assign(Lt.style,{display:"inline-flex",gap:at.lg,alignItems:"center"}),Z.appendChild(Lt);const gt=ee(Lt,l.axisPrefs,s=>{l.onAxisPrefsChange?.(s),j()},r.ownerDocument),C=qt(r);let dt=null,S=null;function et(s){const v=J.find(nt=>nt.testNumber===s);return{unit:v?.unit,limitLow:v?.limitLow,limitHigh:v?.limitHigh}}function j(){const{includeLimits:s,clipOutliers:v}=gt.get();if(dt?.disconnect(),dt=null,y.innerHTML="",d===null){Tt(y,"No parametric test data available for a histogram.");return}const nt=!!B&&B.length>0;let it=!1,u;if(nt){const{limitLow:a,limitHigh:m}=et(d),o=B.flatMap(x=>x.items),{min:e,max:p}=Ct(o,d);if(it=s??Kt(e,p,a,m),v){const x=At(o,d),H=Ft(x);H&&(u={lo:Math.max(e,H.lo),hi:Math.min(p,H.hi)},K=x.reduce((lt,E)=>lt+(E<u.lo||E>u.hi?1:0),0))}u||(K=0)}const $=nt?Ut(B,d,16,it?et(d).limitLow:void 0,it?et(d).limitHigh:void 0,u):null;if(vt.style.display=$?"none":"",$){const{limitLow:a,limitHigh:m}=et(d);if(gt.sync(it,a!==void 0||m!==void 0),$.series.length===0){Tt(y,"No parametric test data available for a histogram.");return}Gt($);return}const _=tt!==null?[q[tt]]:q,{unit:O,limitLow:P,limitHigh:W}=et(d),k=At(_,d),{min:ot,max:mt}=Ct(_,d),ut=s??Kt(ot,mt,P,W);gt.sync(ut,P!==void 0||W!==void 0);const U=v?Ft(k):null,X=U?{lo:Math.max(ot,U.lo),hi:Math.min(mt,U.hi)}:void 0;K=X?k.filter(a=>a<X.lo||a>X.hi).length:0;const h=_t(_,d,16,ut?P:void 0,ut?W:void 0,X);if(h.length===0){Tt(y,"No parametric test data available for a histogram.");return}const D=Math.max(...h.map(a=>a.count),1),Y=r.ownerDocument.createElement("div");Object.assign(Y.style,{fontSize:$t.body,color:Pt.label,marginBottom:at.xxs}),Y.textContent=`max ${D} dies/bucket`+(K?` \xB7 ${K} value${K===1?"":"s"} outside clipped range`:""),y.appendChild(Y);const M=r.ownerDocument.createElement("canvas");M.style.display="block",M.style.cursor="default",y.appendChild(M);let F=-1;const n=h[0].rangeLow,g=h[h.length-1].rangeHigh,L=g-n||1,t=Bt(Math.max(Math.abs(n),Math.abs(g)),O);function c(a){const m=ft+36,o=M.clientWidth-m-ft,e=a-Rt-yt;return{plotX:m,plotMaxWidth:Math.max(10,o),plotMaxHeight:e,plotTop:yt}}function f(a){const{plotX:m,plotMaxWidth:o}=c(wt);if(a<m||a>m+o)return-1;const e=Math.floor((a-m)/o*h.length);return e>=0&&e<h.length?e:-1}function T(){It(M,Y);const a=y.clientWidth,m=Et(r,y,M,wt),o=Ot(M,r,a,m);if(!o)return;const{ctx:e,theme:p}=o,{plotX:x,plotMaxWidth:H,plotMaxHeight:lt,plotTop:E}=c(m),i=E+lt,w=H/h.length;Nt(e,x,E,i,H,D,{text:p.textMuted,axis:p.border,grid:p.border}),h.forEach((b,z)=>{const I=b.count/D*lt,kt=x+z*w,Ht=i-I;e.fillStyle=z===F?p.text:Xt,e.globalAlpha=z===F?1:.7,e.fillRect(kt+1,Ht,Math.max(1,w-2),I),e.globalAlpha=1,z===F&&(e.fillStyle=p.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${b.count}`,kt+w/2,Ht-2))});const A=b=>x+(b-n)/L*H;e.font=`${Q(-1)}px system-ui, sans-serif`;const{offAxis:ht}=ie({dataMin:n,dataMax:g,limitLow:P,limitHigh:W,includeLimits:!1});ne(e,ht,{left:x,right:x+H,top:E,bottom:i},"horizontal",p.limitLine,t.tick);const pt=new Set(ht.map(b=>b.value));for(const[b,z]of[[P,"LSL"],[W,"USL"]]){if(b===void 0||pt.has(b))continue;const I=A(b);e.strokeStyle=p.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(I,E),e.lineTo(I,i),e.stroke(),e.setLineDash([]),e.fillStyle=p.limitLine,e.textAlign="center",e.textBaseline="bottom",e.fillText(z,I,E-2)}e.font=`${Q(-1)}px system-ui, sans-serif`,e.strokeStyle=p.border,e.lineWidth=1,e.beginPath(),e.moveTo(x,i),e.lineTo(x+H,i),e.stroke(),e.fillStyle=p.textMuted,e.textAlign="center",e.textBaseline="top";const st=Math.max(2,Math.floor(H/44)),R=Math.ceil(h.length/st),N=Math.max(1,R);for(let b=0;b<=h.length;b+=N){const z=b<h.length?h[b].rangeLow:h[h.length-1].rangeHigh,I=x+b*w;e.beginPath(),e.moveTo(I,i),e.lineTo(I,i+4),e.stroke(),e.fillText(t.tick(z),I,i+6)}t.unitLabel&&(e.save(),e.font=`${Q(-1)}px system-ui, sans-serif`,e.fillStyle=p.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${t.unitLabel})`,x+H,i+20),e.restore())}M.addEventListener("mousemove",a=>{const m=M.getBoundingClientRect(),o=f(a.clientX-m.left);if(o!==F&&(F=o,T()),o>=0){const e=h[o];C.innerHTML=`<strong>${St(`${bt(e.rangeLow,O,"engineering")} \u2013 ${bt(e.rangeHigh,O,"engineering")}`)}</strong><br>${e.count} dies`,C.style.display="block",Dt(C,r,a.clientX,a.clientY)}else C.style.display="none"}),M.addEventListener("mouseleave",()=>{F!==-1&&(F=-1,T()),C.style.display="none"}),dt=Wt(r,()=>T()),T()}function Gt(s){const{unit:v,limitLow:nt,limitHigh:it}=et(d),u=s.ranges,$=s.series,_=Yt;S&&!$.some(n=>n.groupKey===S)&&(S=null);const O=Math.max(1,...$.flatMap(n=>n.counts)),P=r.ownerDocument.createElement("div");Object.assign(P.style,{fontSize:$t.body,color:Pt.label,marginBottom:at.xxs}),P.textContent=`${$.length} groups \xB7 max ${O} dies/bucket`,y.appendChild(P);const W=r.ownerDocument.createElement("div");Object.assign(W.style,{display:"flex",flexWrap:"wrap",gap:at.sm,marginTop:at.md,marginBottom:at.xl}),$.forEach((n,g)=>{const L=le(n.groupKey,_(g),r.ownerDocument),t=L.el;Jt(t,r,C,`${n.groupKey} \u2014 click to emphasize (dim the rest)`),L.setState({selected:S===n.groupKey,dimmed:S!==null&&S!==n.groupKey}),t.addEventListener("click",()=>{const c=S===n.groupKey?null:n.groupKey;zt(c),l.onGroupChange?.(c)}),W.appendChild(t)}),y.appendChild(W);const k=r.ownerDocument.createElement("canvas");k.style.display="block",k.style.cursor="default",y.appendChild(k);const ot=u[0].rangeLow,mt=u[u.length-1].rangeHigh,ut=mt-ot||1,U=Bt(Math.max(Math.abs(ot),Math.abs(mt)),v),X=n=>{const g=(n.ownerDocument.defaultView??window).getComputedStyle(n);return n.offsetHeight+(parseFloat(g.marginTop)||0)+(parseFloat(g.marginBottom)||0)},h=()=>X(P)+X(W);let D=-1,Y={plotX:ft+36,plotMaxWidth:10,barWidth:10};function M(){It(k,h());const n=y.clientWidth,g=Et(r,y,k,wt),L=Ot(k,r,n,g);if(!L)return;const{ctx:t,theme:c}=L,f=ft+36,T=Math.max(10,n-f-ft),a=yt,m=g-Rt-yt,o=a+m,e=T/u.length;Y={plotX:f,plotMaxWidth:T,barWidth:e},Nt(t,f,a,o,T,O,{text:c.textMuted,axis:c.border,grid:c.border}),D>=0&&(t.fillStyle=c.bgHover,t.globalAlpha=.5,t.fillRect(f+D*e,a,e,m),t.globalAlpha=1);const p=$.map((i,w)=>({s:i,i:w})).sort((i,w)=>(i.s.groupKey===S?1:0)-(w.s.groupKey===S?1:0));for(const{s:i,i:w}of p){const A=S===i.groupKey,ht=S!==null&&!A,pt=_(w);t.strokeStyle=pt,t.fillStyle=pt,t.lineWidth=A?2:1,t.globalAlpha=ht?.12:S?.32:.22,t.beginPath(),t.moveTo(f,o),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/O*m;t.lineTo(R,N),t.lineTo(R+e,N)}),t.lineTo(f+T,o),t.closePath(),t.fill(),t.globalAlpha=ht?.3:1,t.beginPath(),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/O*m;st===0?t.moveTo(R,N):t.lineTo(R,N),t.lineTo(R+e,N)}),t.stroke()}t.globalAlpha=1;const x=i=>f+(i-ot)/ut*T;t.font=`${Q(-1)}px system-ui, sans-serif`;for(const[i,w]of[[nt,"LSL"],[it,"USL"]]){if(i===void 0)continue;const A=x(i);t.strokeStyle=c.limitLine,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(A,a),t.lineTo(A,o),t.stroke(),t.setLineDash([]),t.fillStyle=c.limitLine,t.textAlign="center",t.textBaseline="bottom",t.fillText(w,A,a-2)}t.font=`${Q(-1)}px system-ui, sans-serif`,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.moveTo(f,o),t.lineTo(f+T,o),t.stroke(),t.fillStyle=c.textMuted,t.textAlign="center",t.textBaseline="top";const lt=Math.max(2,Math.floor(T/44)),E=Math.max(1,Math.ceil(u.length/lt));for(let i=0;i<=u.length;i+=E){const w=i<u.length?u[i].rangeLow:u[u.length-1].rangeHigh,A=f+i*e;t.beginPath(),t.moveTo(A,o),t.lineTo(A,o+4),t.stroke(),t.fillText(U.tick(w),A,o+6)}U.unitLabel&&(t.save(),t.font=`${Q(-1)}px system-ui, sans-serif`,t.fillStyle=c.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${U.unitLabel})`,f+T,o+20),t.restore())}function F(n){const{plotX:g,plotMaxWidth:L,barWidth:t}=Y;if(n<g||n>g+L)return-1;const c=Math.floor((n-g)/t);return c>=0&&c<u.length?c:-1}k.addEventListener("mousemove",n=>{const g=k.getBoundingClientRect(),L=F(n.clientX-g.left);if(L!==D&&(D=L,M()),L>=0){const t=u[L],c=$.map((f,T)=>`<span style="${oe(_(T))};margin-right:4px"></span>${St(`${f.groupKey}: ${f.counts[L]}`)}`).join("<br>");C.innerHTML=`<strong>${St(`${bt(t.rangeLow,v,"engineering")} \u2013 ${bt(t.rangeHigh,v,"engineering")}`)}</strong><br>${c}`,C.style.display="block",Dt(C,r,n.clientX,n.clientY)}else C.style.display="none"}),k.addEventListener("mouseleave",()=>{D!==-1&&(D=-1,M()),C.style.display="none"}),dt=Wt(r,()=>M()),M()}j();function Vt(s){V.set(s)&&(d=s,j())}function jt(s){gt.set(s)&&j()}return{card:r,setTest:Vt,setAxisPrefs:jt,destroy:()=>dt?.disconnect()}}
|
|
1
|
+
import{buildTestHistogramData as Ue,collectTestValues as He,buildTestHistogramSeries as Xe,testValueExtent as Ae}from"../../stats/histogram.js";import{SPACE as se,fontPx as X,FONT as Ce,CLR as $e}from"../toolbar.js";import{fmt as pe}from"../../renderer/fmt.js";import{niceStep as Ye,fitTicks as We}from"../../renderer/axisTicks.js";import{QUANTITY as Qe,categorical as qe}from"./palette.js";import{cardShell as Je,observeResize as De,makeTooltip as Ze,attachChartTip as et,positionChartTooltip as Ee,makeLinkedTestSelect as tt,makeWaferSelect as nt,makeLinkedAxisPrefs as it,renderEmptyState as Te,chartFillHeight as Pe,applyCanvasFlow as Ie,makeAxisFormat as Le,horizontalTickSpacing as Be,PADDING as ue,robustFence as Re,shouldIncludeLimitsByDefault as Oe,drawOffAxisLimits as ot,resolveAxisRange as lt,chartSwatchCss as st,makeSeriesLegendItem as at,prepareCanvas as Fe}from"./chartShell.js";import{escHtml as ve,maxOf as rt}from"../../core/utils.js";const Me=230,Ke=36,xe=18;function Ne(o,z,G,Y,be,R,r,y=4){const ae=Math.max(1,Ye(R/y)),me=Y-G;o.save(),o.font=`${X(-1)}px system-ui, sans-serif`,o.textAlign="right",o.textBaseline="middle";for(let c=0;c<=R+1e-9;c+=ae){const F=Y-c/(R||1)*me;o.strokeStyle=r.grid,o.lineWidth=1,o.beginPath(),o.moveTo(z,F),o.lineTo(z+be,F),o.stroke(),o.fillStyle=r.text,o.fillText(String(Math.round(c)),z-4,F)}o.strokeStyle=r.axis,o.lineWidth=1,o.beginPath(),o.moveTo(z,G),o.lineTo(z,Y),o.stroke(),o.restore()}export function renderHistogramPanel(o){const{title:z="Test value distribution",items:G,testDefs:Y,onSaveImage:be,groups:R}=o,{card:r,body:y,controlsRow:ae}=Je(z,be,o.ownerDocument),me=Y.filter(l=>l.testNumber!==void 0);let c=o.selectedTestNumber??me[0]?.testNumber??null,F=null,Q=0;const Se=tt(me,c,l=>{c=l,V(),o.onTestChange?.(l)},{maxWidth:"200px",ownerDocument:r.ownerDocument}),ze=Se.el;ae.appendChild(ze);const fe=G.length>1?nt(G,F,l=>{F=l,V()},{ownerDocument:r.ownerDocument}):null;fe&&ae.appendChild(fe);function Ge(l){return v===l||l!==null&&!(R??[]).some(S=>S.key===l)?!1:(v=l,V(),!0)}const ye=r.ownerDocument.createElement("span");Object.assign(ye.style,{display:"inline-flex",gap:se.lg,alignItems:"center"}),ae.appendChild(ye);const he=it(ye,o.axisPrefs,l=>{o.onAxisPrefsChange?.(l),V()},r.ownerDocument),C=Ze(r);let re=null,v=null;function q(l){const S=Y.find(J=>J.testNumber===l);return{unit:S?.unit,limitLow:S?.limitLow,limitHigh:S?.limitHigh}}function V(){const{includeLimits:l,clipOutliers:S}=he.get();if(re?.disconnect(),re=null,y.innerHTML="",c===null){Te(y,"No parametric test data available for a histogram.");return}const J=!!R&&R.length>0;let Z=!1,k;if(J){const{limitLow:s,limitHigh:u}=q(c),i=R.flatMap(b=>b.items),{min:t,max:x}=Ae(i,c);if(Z=l??Oe(t,x,s,u),S){const b=He(i,c),M=Re(b);M&&(k={lo:Math.max(t,M.lo),hi:Math.min(x,M.hi)},Q=b.reduce((ie,A)=>ie+(A<k.lo||A>k.hi?1:0),0))}k||(Q=0)}const $=J?Xe(R,c,16,Z?q(c).limitLow:void 0,Z?q(c).limitHigh:void 0,k):null;if(fe&&(fe.style.display=$?"none":""),$){const{limitLow:s,limitHigh:u}=q(c);if(he.sync(Z,s!==void 0||u!==void 0),$.series.length===0){Te(y,"No parametric test data available for a histogram.");return}Ve($);return}const j=F!==null?[G[F]]:G,{unit:W,limitLow:D,limitHigh:E}=q(c),w=He(j,c),{min:K,max:ee}=Ae(j,c),te=l??Oe(K,ee,D,E);he.sync(te,D!==void 0||E!==void 0);const ne=S?Re(w):null,_=ne?{lo:Math.max(K,ne.lo),hi:Math.min(ee,ne.hi)}:void 0;Q=_?w.filter(s=>s<_.lo||s>_.hi).length:0;const H=Ue(j,c,16,te?D:void 0,te?E:void 0,_);if(H.length===0){Te(y,"No parametric test data available for a histogram.");return}const P=Math.max(rt(H.map(s=>s.count)),1),U=r.ownerDocument.createElement("div");Object.assign(U.style,{fontSize:Ce.body,color:$e.label,marginBottom:se.xxs}),U.textContent=`max ${P} dies/bucket`+(Q?` \xB7 ${Q} value${Q===1?"":"s"} outside clipped range`:""),y.appendChild(U);const T=r.ownerDocument.createElement("canvas");T.style.display="block",T.style.cursor="default",y.appendChild(T);let O=-1;const n=H[0].rangeLow,f=H[H.length-1].rangeHigh,g=f-n||1,e=Math.max(Math.abs(n),Math.abs(f));function d(s){const u=ue+36,i=T.clientWidth-u-ue,t=s-Ke-xe;return{plotX:u,plotMaxWidth:Math.max(10,i),plotMaxHeight:t,plotTop:xe}}function h(s){const{plotX:u,plotMaxWidth:i}=d(Me);if(s<u||s>u+i)return-1;const t=Math.floor((s-u)/i*H.length);return t>=0&&t<H.length?t:-1}function p(){Ie(T,U);const s=y.clientWidth,u=Pe(r,y,T,Me),i=Fe(T,r,s,u);if(!i)return;const{ctx:t,theme:x}=i,{plotX:b,plotMaxWidth:M,plotMaxHeight:ie,plotTop:A}=d(u),a=A+ie,L=M/H.length;Ne(t,b,A,a,M,P,{text:x.textMuted,axis:x.border,grid:x.border}),H.forEach((m,B)=>{const le=m.count/P*ie,ke=b+B*L,we=a-le;t.fillStyle=B===O?x.text:Qe,t.globalAlpha=B===O?1:.7,t.fillRect(ke+1,we,Math.max(1,L-2),le),t.globalAlpha=1,B===O&&(t.fillStyle=x.text,t.textAlign="center",t.textBaseline="bottom",t.fillText(`${m.count}`,ke+L/2,we-2))});const N=m=>b+(m-n)/g*M;t.font=`${X(-1)}px system-ui, sans-serif`;const{offAxis:ce}=lt({dataMin:n,dataMax:f,limitLow:D,limitHigh:E,includeLimits:!1});ot(t,ce,{left:b,right:b+M,top:A,bottom:a},"horizontal",x.limitLine,Le(e,W).tick);const ge=new Set(ce.map(m=>m.value));for(const[m,B]of[[D,"LSL"],[E,"USL"]]){if(m===void 0||ge.has(m))continue;const le=N(m);t.strokeStyle=x.limitLine,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(le,A),t.lineTo(le,a),t.stroke(),t.setLineDash([]),t.fillStyle=x.limitLine,t.textAlign="center",t.textBaseline="bottom",t.fillText(B,le,A-2)}t.font=`${X(-1)}px system-ui, sans-serif`,t.strokeStyle=x.border,t.lineWidth=1,t.beginPath(),t.moveTo(b,a),t.lineTo(b+M,a),t.stroke(),t.fillStyle=x.textMuted,t.textAlign="center",t.textBaseline="top";const{ticks:de,step:oe}=We(n,f,M,Be(t,e,W)),I=Le(e,W,oe||void 0);for(const m of de){const B=b+(m-n)/g*M;t.beginPath(),t.moveTo(B,a),t.lineTo(B,a+4),t.stroke(),t.fillText(I.tick(m),B,a+6)}I.unitLabel&&(t.save(),t.font=`${X(-1)}px system-ui, sans-serif`,t.fillStyle=x.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${I.unitLabel})`,b+M,a+20),t.restore())}T.addEventListener("mousemove",s=>{const u=T.getBoundingClientRect(),i=h(s.clientX-u.left);if(i!==O&&(O=i,p()),i>=0){const t=H[i];C.innerHTML=`<strong>${ve(`${pe(t.rangeLow,W,"engineering")} \u2013 ${pe(t.rangeHigh,W,"engineering")}`)}</strong><br>${t.count} dies`,C.style.display="block",Ee(C,r,s.clientX,s.clientY)}else C.style.display="none"}),T.addEventListener("mouseleave",()=>{O!==-1&&(O=-1,p()),C.style.display="none"}),re=De(r,()=>p()),p()}function Ve(l){const{unit:S,limitLow:J,limitHigh:Z}=q(c),k=l.ranges,$=l.series,j=qe;v&&!$.some(n=>n.groupKey===v)&&(v=null);const W=Math.max(1,...$.flatMap(n=>n.counts)),D=r.ownerDocument.createElement("div");Object.assign(D.style,{fontSize:Ce.body,color:$e.label,marginBottom:se.xxs}),D.textContent=`${$.length} groups \xB7 max ${W} dies/bucket`,y.appendChild(D);const E=r.ownerDocument.createElement("div");Object.assign(E.style,{display:"flex",flexWrap:"wrap",gap:se.sm,marginTop:se.md,marginBottom:se.xl}),$.forEach((n,f)=>{const g=at(n.groupKey,j(f),r.ownerDocument),e=g.el;et(e,r,C,`${n.groupKey} \u2014 click to emphasize (dim the rest)`),g.setState({selected:v===n.groupKey,dimmed:v!==null&&v!==n.groupKey}),e.addEventListener("click",()=>{const d=v===n.groupKey?null:n.groupKey;Ge(d),o.onGroupChange?.(d)}),E.appendChild(e)}),y.appendChild(E);const w=r.ownerDocument.createElement("canvas");w.style.display="block",w.style.cursor="default",y.appendChild(w);const K=k[0].rangeLow,ee=k[k.length-1].rangeHigh,te=ee-K||1,ne=Math.max(Math.abs(K),Math.abs(ee)),_=n=>{const f=(n.ownerDocument.defaultView??window).getComputedStyle(n);return n.offsetHeight+(parseFloat(f.marginTop)||0)+(parseFloat(f.marginBottom)||0)},H=()=>_(D)+_(E);let P=-1,U={plotX:ue+36,plotMaxWidth:10,barWidth:10};function T(){Ie(w,H());const n=y.clientWidth,f=Pe(r,y,w,Me),g=Fe(w,r,n,f);if(!g)return;const{ctx:e,theme:d}=g,h=ue+36,p=Math.max(10,n-h-ue),s=xe,u=f-Ke-xe,i=s+u,t=p/k.length;U={plotX:h,plotMaxWidth:p,barWidth:t},Ne(e,h,s,i,p,W,{text:d.textMuted,axis:d.border,grid:d.border}),P>=0&&(e.fillStyle=d.bgHover,e.globalAlpha=.5,e.fillRect(h+P*t,s,t,u),e.globalAlpha=1);const x=$.map((a,L)=>({s:a,i:L})).sort((a,L)=>(a.s.groupKey===v?1:0)-(L.s.groupKey===v?1:0));for(const{s:a,i:L}of x){const N=v===a.groupKey,ce=v!==null&&!N,ge=j(L);e.strokeStyle=ge,e.fillStyle=ge,e.lineWidth=N?2:1,e.globalAlpha=ce?.12:v?.32:.22,e.beginPath(),e.moveTo(h,i),a.counts.forEach((de,oe)=>{const I=h+oe*t,m=i-de/W*u;e.lineTo(I,m),e.lineTo(I+t,m)}),e.lineTo(h+p,i),e.closePath(),e.fill(),e.globalAlpha=ce?.3:1,e.beginPath(),a.counts.forEach((de,oe)=>{const I=h+oe*t,m=i-de/W*u;oe===0?e.moveTo(I,m):e.lineTo(I,m),e.lineTo(I+t,m)}),e.stroke()}e.globalAlpha=1;const b=a=>h+(a-K)/te*p;e.font=`${X(-1)}px system-ui, sans-serif`;for(const[a,L]of[[J,"LSL"],[Z,"USL"]]){if(a===void 0)continue;const N=b(a);e.strokeStyle=d.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(N,s),e.lineTo(N,i),e.stroke(),e.setLineDash([]),e.fillStyle=d.limitLine,e.textAlign="center",e.textBaseline="bottom",e.fillText(L,N,s-2)}e.font=`${X(-1)}px system-ui, sans-serif`,e.strokeStyle=d.border,e.lineWidth=1,e.beginPath(),e.moveTo(h,i),e.lineTo(h+p,i),e.stroke(),e.fillStyle=d.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:M,step:ie}=We(K,ee,p,Be(e,ne,S)),A=Le(ne,S,ie||void 0);for(const a of M){const L=h+(a-K)/te*p;e.beginPath(),e.moveTo(L,i),e.lineTo(L,i+4),e.stroke(),e.fillText(A.tick(a),L,i+6)}A.unitLabel&&(e.save(),e.font=`${X(-1)}px system-ui, sans-serif`,e.fillStyle=d.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${A.unitLabel})`,h+p,i+20),e.restore())}function O(n){const{plotX:f,plotMaxWidth:g,barWidth:e}=U;if(n<f||n>f+g)return-1;const d=Math.floor((n-f)/e);return d>=0&&d<k.length?d:-1}w.addEventListener("mousemove",n=>{const f=w.getBoundingClientRect(),g=O(n.clientX-f.left);if(g!==P&&(P=g,T()),g>=0){const e=k[g],d=$.map((h,p)=>`<span style="${st(j(p))};margin-right:4px"></span>${ve(`${h.groupKey}: ${h.counts[g]}`)}`).join("<br>");C.innerHTML=`<strong>${ve(`${pe(e.rangeLow,S,"engineering")} \u2013 ${pe(e.rangeHigh,S,"engineering")}`)}</strong><br>${d}`,C.style.display="block",Ee(C,r,n.clientX,n.clientY)}else C.style.display="none"}),w.addEventListener("mouseleave",()=>{P!==-1&&(P=-1,T()),C.style.display="none"}),re=De(r,()=>T()),T()}V();function je(l){Se.set(l)&&(c=l,V())}function _e(l){he.set(l)&&V()}return{card:r,setTest:je,setAxisPrefs:_e,destroy:()=>re?.disconnect()}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseRegionKey as
|
|
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 +1 @@
|
|
|
1
|
-
import{NO_DATA_FILL as
|
|
1
|
+
import{NO_DATA_FILL as ce}from"../../renderer/colorMap.js";import{maxOf as de,minOf as me}from"../../core/utils.js";import{categorical as ve}from"./palette.js";import{buildScatterData as Ce,buildScatterDataGrouped as Ae}from"../../stats/scatter.js";import{pearsonOfPairs as De}from"../../stats/correlation.js";import{buildFacetTable as Oe}from"../../stats/facets.js";import{SPACE as S,RADIUS as Pe,fontPx as W,FONT as V,CLR as F}from"../toolbar.js";import{fitTicks as fe}from"../../renderer/axisTicks.js";import{cardShell as $e,observeResize as Be,makeTooltip as Ee,attachChartTip as He,makeTestSelect as Ie,makeWaferSelect as We,chartFillHeight as Fe,applyCanvasFlow as Ne,drawAxisUnit as Re,resolveChartCanvasColors as ze,makeAxisFormat as pe,horizontalTickSpacing as _e,VERTICAL_TICK_SPACING_PX as Ue,makeSeriesLegendItem as je,prepareCanvas as Ge}from"./chartShell.js";const r=52,Xe=16,l=16,Ye=44;export function renderScatterPanel(N){const{title:ue="Test scatter",items:v,testDefs:q,binColors:xe,onSaveImage:he,groups:R}=N,{card:i,body:M,controlsRow:J}=$e(ue,he,N.ownerDocument),z=q.filter(t=>t.testNumber!==void 0),y=!!(R&&R.length>0),Q=(R??[]).map(t=>t.key),ge=new Map(Q.map((t,n)=>[t,n]));let f=N.xTestNumber??z[0]?.testNumber??null,x=N.yTestNumber??z[1]?.testNumber??f,C=null;const Z=()=>C!==null?v[C]?[v[C]]:[]:v;function ee(t,n,o){const s=i.ownerDocument.createElement("label");Object.assign(s.style,{display:"inline-flex",alignItems:"center",gap:S.xs,fontSize:V.body,color:F.label});const g=i.ownerDocument.createElement("span");g.textContent=t;const c=Ie(z,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:i.ownerDocument});return s.append(g,c),{wrap:s,select:c}}const{wrap:be,select:ye}=ee("X:",f,t=>{f=t,$()}),{wrap:Te,select:Se}=ee("Y:",x,t=>{x=t,$()});J.append(be,Te),!y&&v.length>1&&J.appendChild(We(v,C,t=>{C=t,$()},{ownerDocument:i.ownerDocument}));const A=i.ownerDocument.createElement("div");Object.assign(A.style,{color:F.warnText,background:F.warnBg,border:`1px solid ${F.warnBorder}`,borderRadius:Pe.control,padding:`${S.xs} ${S.md}`,fontSize:V.body,marginBottom:S.xs,display:"none"}),i.insertBefore(A,M);function te(){const t=y?[]:Oe(Z(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);t.length>0?(A.textContent=`\u26A0 Mixed ${t.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.`,A.style.display=""):A.style.display="none"}te();const j=i.ownerDocument.createElement("div");Object.assign(j.style,{color:F.label,fontSize:V.body,marginBottom:S.xs}),i.insertBefore(j,M);function G(){const t=y?"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=h.size===0?p:p.filter(g=>h.has(_(g))),{r:o,n:s}=De(n);j.textContent=o===null?`${t} \xB7 n = ${s.toLocaleString()}`:`${t} \xB7 r = ${o.toFixed(3)} \xB7 n = ${s.toLocaleString()}`}const _=t=>y?t.group??"\u2014":String(t.hbin??0),ne=t=>y?ve(ge.get(t)??0):t==="0"?ce:xe?.get(Number(t))??ce,oe=t=>y?t:t==="0"?"No bin data":`Bin ${t}`,h=new Set,D=i.ownerDocument.createElement("div");Object.assign(D.style,{display:"flex",flexWrap:"wrap",gap:S.sm,marginTop:S.md,marginBottom:S.xl}),M.appendChild(D);const k=i.ownerDocument.createElement("canvas");k.style.display="block",k.style.cursor="crosshair",M.appendChild(k);const we=Ee(i);let p=[],b=0,L=1,T=0,O=1;function P(t){const n=q.find(o=>o.testNumber===t);return{unit:n?.unit,limitLow:n?.limitLow,limitHigh:n?.limitHigh}}function Me(){const t=M.clientWidth,n=Math.max(200,Math.min(400,t*.65)),o=Fe(i,M,k,n);return{w:t,h:o,plotW:Math.max(10,t-r-Xe),plotH:Math.max(10,o-l-Ye)}}const X=new Map;function ie(){for(const[t,n]of X){const o=h.has(t);n.setState({selected:o,dimmed:h.size>0&&!o})}}function se(t){D.innerHTML="",X.clear(),h.clear();for(const n of t){const o=je(oe(n),ne(n),i.ownerDocument),s=o.el;s.dataset.cat=n,X.set(n,o),He(s,i,we,`${oe(n)} \u2014 click to filter`),s.addEventListener("click",()=>{h.has(n)?h.delete(n):h.add(n),ie(),G(),U()}),D.appendChild(s)}ie()}function U(){Ne(k,D);const t=ze(i),n=L-b,o=O-T,{w:s,h:g,plotW:c,plotH:a}=Me(),B=Ge(k,i,s,g);if(!B)return;const{ctx:e}=B;if(p.length===0){e.font=`${W()}px system-ui, sans-serif`,e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.fillText("No data \u2014 select two parametric tests with values.",s/2,g/2);return}e.font=`${W(-1)}px system-ui, sans-serif`;const E=f!==null?P(f).unit:void 0,le=fe(b,L,c,_e(e,Math.max(Math.abs(b),Math.abs(L)),E)),ae=fe(T,O,a,()=>Ue),Y=pe(Math.max(Math.abs(b),Math.abs(L)),f!==null?P(f).unit:void 0,le.step||void 0),K=pe(Math.max(Math.abs(T),Math.abs(O)),x!==null?P(x).unit:void 0,ae.step||void 0);e.font=`${W(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=.5,e.fillStyle=t.textMuted;for(const d of le.ticks){const m=r+(d-b)/n*c;e.beginPath(),e.moveTo(m,l),e.lineTo(m,l+a),e.stroke(),e.textAlign="center",e.textBaseline="top",e.fillText(Y.tick(d),m,l+a+4)}for(const d of ae.ticks){const m=l+(1-(d-T)/o)*a;e.beginPath(),e.moveTo(r,m),e.lineTo(r+c,m),e.stroke(),e.textAlign="right",e.textBaseline="middle",e.fillText(K.tick(d),r-4,m)}Y.unitLabel&&Re(e,Y.unitLabel,r+c/2,l+a+24,t.textMuted),K.unitLabel&&(e.save(),e.font=`${W(-1)}px system-ui, sans-serif`,e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.translate(6,l+a/2),e.rotate(-Math.PI/2),e.fillText(`(${K.unitLabel})`,0,0),e.restore());const H=h.size===0?p:p.filter(d=>h.has(_(d))),re=H.length>5e3?Math.ceil(H.length/5e3):1;e.globalAlpha=Math.max(.15,Math.min(.7,200/(H.length/re)));for(let d=0;d<H.length;d+=re){const m=H[d],u=r+(m.x-b)/n*c,I=l+(1-(m.y-T)/o)*a;e.beginPath(),e.arc(u,I,2.5,0,Math.PI*2),e.fillStyle=ne(_(m)),e.fill()}if(e.globalAlpha=1,f!==null&&x!==null){const d=P(f),m=P(x);e.strokeStyle=t.textMuted,e.fillStyle=t.textMuted,e.lineWidth=1,e.setLineDash([4,3]),e.font=`${W(-1)}px system-ui, sans-serif`,e.textBaseline="top";for(const[u,I]of[[d.limitLow,"LSL"],[d.limitHigh,"USL"]]){if(u===void 0||u<b||u>L)continue;const w=r+(u-b)/n*c;e.beginPath(),e.moveTo(w,l),e.lineTo(w,l+a),e.stroke(),e.textAlign="left",e.fillText(I,w+2,l+2)}for(const[u,I]of[[m.limitLow,"LSL"],[m.limitHigh,"USL"]]){if(u===void 0||u<T||u>O)continue;const w=l+(1-(u-T)/o)*a;e.beginPath(),e.moveTo(r,w),e.lineTo(r+c,w),e.stroke(),e.textAlign="left",e.fillText(I,r+3,w+2)}e.setLineDash([])}e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(r,l),e.lineTo(r,l+a),e.moveTo(r,l+a),e.lineTo(r+c,l+a),e.stroke()}function $(){if(te(),z.length<2||f===null||x===null){p=[],se([]),G(),U();return}p=y?Ae(R,f,x):Ce(Z(),f,x);let t;if(y){const n=new Set(p.map(_));t=Q.filter(o=>n.has(o))}else t=Array.from(new Set(p.map(n=>n.hbin??0))).sort((n,o)=>n-o).map(String);if(se(t),G(),p.length>0){const n=p.map(E=>E.x),o=p.map(E=>E.y),s=me(n),g=de(n),c=me(o),a=de(o),B=(g-s)*.05||1,e=(a-c)*.05||1;b=s-B,L=g+B,T=c-e,O=a+e}U()}const ke=Be(i,()=>U());$();function Le(t,n){f=t,x=n,ye.value=String(t),Se.value=String(n),$()}return{card:i,setXY:Le,destroy:()=>ke.disconnect()}}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type SweepSpec } from '../../stats/sweep.js';
|
|
2
|
+
import type { Die } from '../../core/dies.js';
|
|
3
|
+
import type { TestDef } from '../../renderer/buildWaferMap.js';
|
|
4
|
+
import { type SaveImageHandler } from './chartShell.js';
|
|
5
|
+
export interface SweepPanelOptions {
|
|
6
|
+
/** The curve definition. Title comes from here, not from the panel. */
|
|
7
|
+
spec: SweepSpec;
|
|
8
|
+
/** Population to aggregate over — already scoped by the caller. Scope is not
|
|
9
|
+
* a property of the sweep: the same definition is valid for any population,
|
|
10
|
+
* which is what keeps a saved spec portable. */
|
|
11
|
+
dies: Die[];
|
|
12
|
+
/**
|
|
13
|
+
* Who `dies` are, read straight after the count — `"selected on W03"` gives
|
|
14
|
+
* "median across 12 dies selected on W03". Omit when the dies are simply the
|
|
15
|
+
* view's own population (the Insights tab, which states its scope itself).
|
|
16
|
+
* A sweep of a hand-picked subset looks exactly like a whole-wafer one, so
|
|
17
|
+
* a drilldown must always pass this.
|
|
18
|
+
*/
|
|
19
|
+
population?: string;
|
|
20
|
+
testDefs: TestDef[] | undefined;
|
|
21
|
+
onSaveImage?: SaveImageHandler;
|
|
22
|
+
ownerDocument?: Document;
|
|
23
|
+
}
|
|
24
|
+
export interface SweepPanelHandle {
|
|
25
|
+
card: HTMLElement;
|
|
26
|
+
destroy: () => void;
|
|
27
|
+
/** Re-aggregate over a new population, keeping the same curve definition. */
|
|
28
|
+
setDies: (dies: Die[]) => void;
|
|
29
|
+
}
|
|
30
|
+
export declare function renderSweepPanel(options: SweepPanelOptions): SweepPanelHandle;
|
|
31
|
+
//# sourceMappingURL=sweep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{buildSweepData as ce}from"../../stats/sweep.js";import{fmt as C}from"../../renderer/fmt.js";import{SPACE as ne,fontPx as fe,FONT as oe,CLR as G,ALPHA as pe}from"../toolbar.js";import{categorical as de}from"./palette.js";import{cardShell as he,observeResize as ue,makeTooltip as me,positionChartTooltip as xe,renderEmptyState as se,chartFillHeight as ge,populationPhrase as be,PADDING as Y,prepareCanvas as ye,makeAxisFormat as $e,VERTICAL_TICK_SPACING_PX as Te}from"./chartShell.js";import{fitTicks as ve}from"../../renderer/axisTicks.js";import{escHtml as u}from"../../core/utils.js";import{unmarkedLabel as Se,derivedTestNote as Me,DERIVED_MARK as J,DERIVED_KEY as Le}from"../../renderer/testLabel.js";function _(m,a){return a?C(m,a).replace(/(\.\d*?)0+(?=\D|$)/,"$1").replace(/\.(?=\D|$)/,""):Number.isInteger(m)?String(m):String(Number(m.toPrecision(3)))}function Pe(m){const a=Se(m.label),S=a.length>9?`${a.slice(0,8)}\u2026`:a;return m.derived?`${J} ${S}`:S}const ie=260,we=62,ke=56,Ae=3;function Ee(m,a){const S=m.series[0]?.points[0]?.testNumber;return S===void 0?void 0:a?.find(j=>j.testNumber===S)?.unit}export function renderSweepPanel(m){const{spec:a,testDefs:S,onSaveImage:j,population:re}=m;let z=m.dies;const{card:y,body:M}=he(a.title,j,m.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=me(y);let X=null,E=null,$;const H=n=>a.series[n]?.color??de(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),$=Ee(E,S),E.series.filter(i=>i.points.some(T=>T.count>0)).length===0){N.textContent="",se(M,"No values recorded for the tests in this sweep."),Z();return}N.textContent=`Line = median across ${be(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 P=()=>{const i=E;if(i===null)return;const T=Math.max(1,M.clientWidth),B=Math.max(ie,ge(y,M,c,ie)),F=ye(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=`${fe(-1)}px system-ui, sans-serif`;const r=Y+we,p=T-Y-8,R=Y,w=B-ke,k=t=>w-(t-f)/(h-f)*(w-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,w),e.lineTo(p,w),e.stroke(),e.fillStyle=x.text,e.textAlign="right",e.textBaseline="middle";const q=ve(f,h,w-R,()=>Te),le=$e(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),Ae,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,w),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),w+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+w)/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})};P(),X=ue(M,P),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} `:""}${u(l.label)}: <strong>${u(C(p.median,$))}</strong><br><span style="opacity:${pe.muted}">p10 ${u(C(p.p10,$))} \u2013 p90 ${u(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=>Me(l)).filter(l=>l!==void 0))];L.innerHTML=`<strong>${u(b?.label??"")}</strong><br>`+h.join("<br>")+v.map(l=>`<br><em>${u(l)}</em>`).join(""),L.style.display="block",xe(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 ${u(n.notMeasured)}`):n.crossing?c.push(`<strong>Crossing:</strong> ${u(`${n.xLabel} = ${_(n.crossing.x,n.xUnit)}`)} \xB7 ${u(`${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 P=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 ${u(C(o.y,$))}:</strong> ${u(P)}`)}else{const P=o.reason==="second-series-never-reaches"?n.series[1]?.label:n.series[0]?.label;c.push(`<strong>Width at ${u(C(o.y,$))}:</strong> not measurable \u2014 ${u(P??"a series")} never reaches this level`)}n.series.some(o=>o.points.some(P=>P.derived))&&c.push(`${J} ${u(Le)} \u2014 hover a point for the expression`);for(const o of n.warnings)c.push(`<span style="color:${G.errText}">${u(o)}</span>`);W.innerHTML=c.join("<br>")}return Q(),{card:y,destroy:()=>{X?.disconnect(),L.remove(),y.remove()},setDies:n=>{z=n,Q()}}}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type TrendItem } from '../../stats/trend.js';
|
|
2
2
|
import { type TestDef } from '../../renderer/buildWaferMap.js';
|
|
3
|
-
import { type AxisPrefs, type SaveImageHandler } from './chartShell.js';
|
|
3
|
+
import { type AxisPrefs, type SaveImageHandler, type WaferContextMenuHandler } from './chartShell.js';
|
|
4
4
|
export interface TrendPanelOptions {
|
|
5
5
|
title?: string;
|
|
6
6
|
items: TrendItem[];
|
|
@@ -9,6 +9,8 @@ export interface TrendPanelOptions {
|
|
|
9
9
|
onSaveImage?: SaveImageHandler;
|
|
10
10
|
/** Click a point to open that wafer's map on this test. */
|
|
11
11
|
onOpen?: (key: number, testNumber: number) => void;
|
|
12
|
+
/** Right-click on a wafer's point — see `WaferContextMenuHandler`. */
|
|
13
|
+
onWaferContextMenu?: WaferContextMenuHandler;
|
|
12
14
|
/**
|
|
13
15
|
* What the dashed reference line is, as a noun phrase — "lot mean" when the
|
|
14
16
|
* items are one lot, else "mean of all wafers" (see stats/population.ts).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildTestTrendData as
|
|
1
|
+
import{buildTestTrendData as he,trendCentre as pe}from"../../stats/trend.js";import{isParametricTest as xe}from"../../renderer/buildWaferMap.js";import{fmt as D}from"../../renderer/fmt.js";import{SPACE as le,fontPx as ge,FONT as ye,CLR as be}from"../toolbar.js";import{QUANTITY as U}from"./palette.js";import{cardShell as Te,observeResize as ve,makeTooltip as Le,positionChartTooltip as ke,makeLinkedTestSelect as we,makeLinkedAxisPrefs as Ce,renderEmptyState as X,chartFillHeight as Ae,PADDING as z,resolveAxisRange as Me,shouldIncludeLimitsByDefault as Se,drawOffAxisLimits as Pe,makeAxisFormat as Ne,VERTICAL_TICK_SPACING_PX as Ee,WAFER_MENU_HINT as He,prepareCanvas as Ie}from"./chartShell.js";import{fitTicks as De}from"../../renderer/axisTicks.js";import{escHtml as G}from"../../core/utils.js";const se=220,Oe=56,We=34,$e=3.5;export function renderTrendPanel(f){const{items:j,testDefs:re,onSaveImage:ae,onOpen:C,centreLabel:Y="mean of all wafers"}=f,ce=f.title??"Wafer-to-wafer trend",{card:s,body:u,controlsRow:K}=Te(ce,ae,f.ownerDocument);s.style.alignSelf="start";const N=re.filter(o=>o.testNumber!==void 0&&xe(o));let h=f.selectedTestNumber??N[0]?.testNumber??null,O=0;const Q=we(N,h,o=>{h=o,A(),f.onTestChange?.(o)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:s.ownerDocument});K.appendChild(Q.el);const W=s.ownerDocument.createElement("span");Object.assign(W.style,{display:"inline-flex",gap:le.lg,alignItems:"center"}),K.appendChild(W);const $=Ce(W,f.axisPrefs,o=>{f.onAxisPrefsChange?.(o),A()},s.ownerDocument),w=s.ownerDocument.createElement("div");Object.assign(w.style,{color:be.label,fontSize:ye.body,marginBottom:le.sm}),s.insertBefore(w,u);const g=Le(s);let E=null,i=[];function me(o){return N.find(R=>R.testNumber===o)}function A(){const{includeLimits:o,clipOutliers:R}=$.get();if(u.innerHTML="",E?.disconnect(),E=null,N.length===0||h===null){w.textContent="",X(u,"No parametric test data available for a trend.");return}if(j.length<2){w.textContent="",X(u,"A wafer-to-wafer trend needs at least two wafers.");return}if(i=he(j,h),!i.some(l=>l.count>0)){w.textContent="",X(u,"No values recorded for this test.");return}const r=me(h),y=pe(i),V=()=>{w.textContent=`Point = wafer mean, whisker = \xB11\u03C3, dashed = ${Y} \xB7 wafers in slot order`+(C?" \xB7 click a point to open that wafer":"")+(O?` \xB7 axis clipped, ${O} outside`:"")};V();const d=s.ownerDocument.createElement("canvas");d.style.display="block",u.appendChild(d);let M=null;const q=()=>{const l=Math.max(1,u.clientWidth),a=Math.max(se,Ae(s,u,d,se)),n=Ie(d,s,l,a);if(!n)return;const{ctx:e,theme:b}=n;let S=1/0,P=-1/0;const J=[];for(const t of i)!t.count||!Number.isFinite(t.mean)||(S=Math.min(S,t.mean-t.stddev),P=Math.max(P,t.mean+t.stddev),J.push(t.mean-t.stddev,t.mean+t.stddev));if(!Number.isFinite(S)||!Number.isFinite(P))return;const Z=o??Se(S,P,r?.limitLow,r?.limitHigh);$.sync(Z,r?.limitLow!==void 0||r?.limitHigh!==void 0);const H=Me({dataMin:S,dataMax:P,limitLow:r?.limitLow,limitHigh:r?.limitHigh,includeLimits:Z,clipOutliers:R,values:J});O=H.clippedCount,V();let x=H.lo,T=H.hi;const ee=(T-x)*.08;x-=ee,T+=ee;const c=z+Oe;e.font=`${ge(-1)}px system-ui, sans-serif`;const te=t=>t.length>8?`${t.slice(0,7)}\u2026`:t,de=i.length?Math.ceil(Math.max(e.measureText(te(i[0].label)).width,e.measureText(te(i[i.length-1].label)).width)/2):0,v=l-z-de,I=z,L=a-We,k=t=>L-(t-x)/(T-x)*(L-I),ne=i.length>1?(v-c)/(i.length-1):0;M={left:c,step:ne};const _=t=>i.length>1?c+t*ne:(c+v)/2;e.strokeStyle=b.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,I),e.lineTo(c,L),e.lineTo(v,L),e.stroke(),e.fillStyle=b.text,e.textAlign="right",e.textBaseline="middle";const ie=De(x,T,L-I,()=>Ee),fe=Ne(Math.max(Math.abs(x),Math.abs(T)),r?.unit,ie.step||void 0);for(const t of ie.ticks)e.fillText(fe.tickWithUnit(t),c-6,k(t));Pe(e,H.offAxis,{left:c,right:v,top:I,bottom:L},"vertical",b.limitLine,t=>D(t,r?.unit));for(const[t,p]of[[r?.limitLow,"LSL"],[r?.limitHigh,"USL"]]){if(t===void 0||t<x||t>T)continue;const m=k(t);e.save(),e.strokeStyle=b.limitLine,e.globalAlpha=.7,e.setLineDash([4,3]),e.beginPath(),e.moveTo(c,m),e.lineTo(v,m),e.stroke(),e.restore(),e.save(),e.fillStyle=b.limitLine,e.textAlign="left",e.fillText(p,c+3,m-6),e.restore()}if(y!==null&&y>=x&&y<=T){const t=k(y);e.save(),e.strokeStyle=b.text,e.globalAlpha=.5,e.setLineDash([3,3]),e.beginPath(),e.moveTo(c,t),e.lineTo(v,t),e.stroke(),e.restore()}e.strokeStyle=U,e.lineWidth=1.5,e.beginPath();let B=!1;i.forEach((t,p)=>{if(!t.count||!Number.isFinite(t.mean)){B=!1;return}const m=_(p),oe=k(t.mean);B?e.lineTo(m,oe):e.moveTo(m,oe),B=!0}),e.stroke(),i.forEach((t,p)=>{if(!t.count||!Number.isFinite(t.mean))return;const m=_(p);t.stddev>0&&(e.save(),e.strokeStyle=U,e.globalAlpha=.55,e.lineWidth=1,e.beginPath(),e.moveTo(m,k(t.mean-t.stddev)),e.lineTo(m,k(t.mean+t.stddev)),e.stroke(),e.restore()),e.fillStyle=U,e.beginPath(),e.arc(m,k(t.mean),$e,0,Math.PI*2),e.fill()}),e.fillStyle=b.text,e.textBaseline="top",e.textAlign="center";const ue=Math.max(1,Math.ceil(i.length*42/Math.max(1,v-c)));i.forEach((t,p)=>{if(p%ue!==0&&p!==i.length-1)return;const m=t.label.length>8?`${t.label.slice(0,7)}\u2026`:t.label;e.fillText(m,_(p),L+6)})},F=l=>{if(!M||M.step<=0)return-1;const a=d.getBoundingClientRect(),n=Math.round((l.clientX-a.left-M.left)/M.step);return n>=0&&n<i.length&&i[n].count>0?n:-1};d.addEventListener("mousemove",l=>{const a=F(l);if(a<0){g.style.display="none",d.style.cursor="default";return}const n=i[a];d.style.cursor=C&&n.key!==void 0?"pointer":"default",g.innerHTML=`<strong>${G(n.label)}</strong><br>`+G(`mean ${D(n.mean,r?.unit)} \xB7 \u03C3 ${D(n.stddev,r?.unit)}`)+`<br>n = ${n.count.toLocaleString()}`+(y!==null?"<br>"+G(`\u0394 vs ${Y} ${n.mean-y>=0?"+":""}${D(n.mean-y,r?.unit)}`):"")+(C&&n.key!==void 0?"<br><em>click to open this wafer</em>":"")+(f.onWaferContextMenu&&n.key!==void 0?He:""),g.style.display="block",ke(g,s,l.clientX,l.clientY)}),d.addEventListener("mouseleave",()=>{g.style.display="none"}),d.addEventListener("click",l=>{const a=F(l);if(a<0||!C)return;const n=i[a];n.key!==void 0&&h!==null&&C(n.key,h)}),d.addEventListener("contextmenu",l=>{const a=F(l),n=a<0?void 0:i[a].key;n===void 0||!f.onWaferContextMenu||(g.style.display="none",f.onWaferContextMenu(n,h??void 0,l))}),q(),E=ve(u,q)}return A(),{card:s,destroy:()=>{E?.disconnect(),g.remove(),s.remove()},setTest:o=>{Q.set(o)&&(h=o,A())},setAxisPrefs:o=>{$.set(o)&&A()}}}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Chunked } from '../core/utils.js';
|
|
2
|
+
/** Handle on a running {@link runChunked} computation. */
|
|
3
|
+
export interface ChunkedRun {
|
|
4
|
+
/** True once the work has finished — set synchronously when it finished
|
|
5
|
+
* inside the first slice, so a caller can tell "already done" from "staged". */
|
|
6
|
+
readonly done: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Abandon the rest of the work. Safe to call at any time, including after it
|
|
9
|
+
* has finished. Whatever the work has already written (DOM appended so far,
|
|
10
|
+
* for instance) is left as it is — the caller owns that, and in practice
|
|
11
|
+
* clears it by starting the next render.
|
|
12
|
+
*/
|
|
13
|
+
cancel(): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Drive a {@link Chunked} computation on the main thread in time-boxed slices,
|
|
17
|
+
* yielding to the event loop between them so the browser can paint, keep
|
|
18
|
+
* responding to input, and — the reason this exists — never show its "page
|
|
19
|
+
* unresponsive" dialog.
|
|
20
|
+
*
|
|
21
|
+
* **The first slice is deliberately generous.** Work that finishes inside it
|
|
22
|
+
* never yields at all, so a small lot's panel renders in one synchronous pass
|
|
23
|
+
* exactly as it always did: no flash of half a panel, no extra frame, nothing
|
|
24
|
+
* for a caller to special-case. Only work that genuinely overruns starts
|
|
25
|
+
* staging, which is the case the staging is for.
|
|
26
|
+
*
|
|
27
|
+
* A slice always runs at least one step before checking its budget, because a
|
|
28
|
+
* step is atomic — the budget bounds how many steps share a task, never how
|
|
29
|
+
* long one step may take. **Keeping each step short is the generator's job**,
|
|
30
|
+
* and it is what actually bounds the longest task.
|
|
31
|
+
*
|
|
32
|
+
* `setTimeout(0)`, not `requestAnimationFrame`: a slice may overrun a frame, and
|
|
33
|
+
* rAF in a background tab stops firing entirely, which would strand a render.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runChunked<T>(work: Chunked<T>, opts?: {
|
|
36
|
+
/** Budget for the synchronous first slice. Default 150 ms — below the ~250 ms
|
|
37
|
+
* at which a wait starts reading as a stall, so anything under it is better
|
|
38
|
+
* finished than staged. Pass 0 to force staging (what the tests do). */
|
|
39
|
+
firstSliceMs?: number;
|
|
40
|
+
/** Budget for every slice after the first. Default 16 ms — one frame. */
|
|
41
|
+
sliceMs?: number;
|
|
42
|
+
/** Called with the result when the work finishes. Not called if cancelled. */
|
|
43
|
+
onDone?: (result: T) => void;
|
|
44
|
+
}): ChunkedRun;
|
|
45
|
+
//# sourceMappingURL=chunked.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function f(){return typeof performance<"u"?performance.now():Date.now()}export function runChunked(r,i={}){const{firstSliceMs:c=150,sliceMs:d=16,onDone:s}=i;let n=!1,t=!1,e;const u=l=>{if(e=void 0,n)return;const a=f();for(;;){const o=r.next();if(o.done){t=!0,s?.(o.value);return}if(f()-a>=l)break}e=setTimeout(()=>u(d),0)};return u(c),{get done(){return t},cancel(){n||t||(n=!0,e!==void 0&&(clearTimeout(e),e=void 0),r.return(void 0))}}}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{hasPosition as
|
|
1
|
+
import{hasPosition as I,isPositionedDie as B}from"../core/dies.js";import{classifyDie as W}from"../core/classify.js";import{isParametricTest as G,getTestPassStatus as S}from"../renderer/buildWaferMap.js";import{resolveMetadataColumns as Y}from"../stats/metadataColumns.js";import{LEADING as N,TRACKING as _,wireControlHover as U,controlStyle as X,SPACE as Q,RADIUS as F,FONT as g,CLR as c,saveTextFile as q}from"./toolbar.js";import{csvField as H}from"./summaryPanel.js";import{fmt as P}from"../renderer/fmt.js";import{testLabel as E,markedTestLabel as j,derivedFields as J,derivedKeyText as Z,derivedTestSource as ee,isDerivedTest as te,DERIVED_KEY as ne}from"../renderer/testLabel.js";const L=(t,r)=>r?`${t} (${r})`:t,ie=5e4,M=new WeakSet;function le(t){if(M.has(t))return;M.add(t);const r=t.createElement("style");r.textContent=`
|
|
2
2
|
.wmap-dielist-table { width: 100%; border-collapse: collapse; font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
3
3
|
.wmap-dielist-th {
|
|
4
4
|
text-align: left; padding: 4px 8px; position: sticky; top: 0;
|
|
@@ -9,5 +9,5 @@ import{hasPosition as R,isPositionedDie as O}from"../core/dies.js";import{classi
|
|
|
9
9
|
padding: 4px 8px; color: ${c.text};
|
|
10
10
|
border-bottom: 1px solid ${c.menuBorder}; white-space: nowrap;
|
|
11
11
|
}
|
|
12
|
-
`,t.head.appendChild(
|
|
12
|
+
`,t.head.appendChild(r)}function s(t,r,n,i){const a=t.createElement(r);return n&&Object.assign(a.style,n),i!==void 0&&(a.textContent=i),a}function ae(t){return I(t)?String(t.x):""}function re(t){return I(t)?String(t.y):""}function oe(t,r){if(r!==void 0)return r.filter(i=>i.testNumber!==void 0);const n=new Map;for(const i of t)for(const a of Object.keys(i.testValues??{})){const d=Number(a);n.has(d)||n.set(d,{testNumber:d,name:E(void 0,d)})}return[...n.values()].sort((i,a)=>i.testNumber-a.testNumber)}function se(t,r,n){if(!r)return;const i=new Map;for(const a of t){if(!B(a))continue;const d=r(a);d&&i.set(a,W(a,d,{ringCount:n}))}return i.size>0?i:void 0}export function buildDieListSection(t,r,n={}){if(!t.length)return null;const i=n.ownerDocument??document;le(i);const a=oe(t,r),d=n.maxRows??ie,C=Math.max(0,t.length-d),z=C>0?t.slice(0,d):t,f=s(i,"div",{display:"flex",flexDirection:"column",gap:Q.md,flex:"1",minHeight:"0",minWidth:"0"}),v=s(i,"div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});v.appendChild(s(i,"div",{fontSize:g.meta,fontWeight:"700",letterSpacing:_,textTransform:"uppercase",color:c.label},n.title??`Die list (${t.length})`));const h=s(i,"button",{fontSize:g.body,padding:"3px 8px",borderRadius:F.control,...X("outlined"),background:c.menuBg},"Export CSV");h.type="button",U(h),v.appendChild(h),f.appendChild(v),n.note&&f.appendChild(s(i,"div",{fontSize:g.body,color:c.label,lineHeight:N.base,flexShrink:"0"},n.note));const w=se(t,n.getWafer,n.ringCount??4),K=t.some(e=>e.edgeExcluded===!0),O=[...n.extraColumn?[n.extraColumn.label]:[],"X","Y","Ring","Quadrant","Edge excluded","Site","Hard bin","Soft bin",...a.map(e=>L(E(e,e.testNumber),e.unit))],{columns:V,truncatedKeys:x}=Y({dies:t,waferMetadata:n.waferMetadata,metadataFields:n.metadataFields,dieKeys:n.metadataColumns,waferKeys:n.waferMetadataColumns==="none"?"none":void 0,waferPlacement:n.waferMetadataColumns??"csv",reservedLabels:O}),y=[...n.extraColumn?[{label:n.extraColumn.label,get:e=>n.extraColumn.get(e)??""}]:[],{label:"X",get:ae},{label:"Y",get:re},...w?[{label:"Ring",get:e=>{const l=w.get(e);return l?String(l.ring):""}},{label:"Quadrant",get:e=>w.get(e)?.quadrant??""}]:[],...K?[{label:"Edge excluded",get:e=>e.edgeExcluded?"Yes":"No"}]:[],{label:"Site",get:e=>e.siteNum!==void 0?String(e.siteNum):""},{label:"Hard bin",get:e=>e.hbin!==void 0?String(e.hbin):""},{label:"Soft bin",get:e=>e.sbin!==void 0?String(e.sbin):""},...a.map(e=>{const l=!G(e);return{label:L(j(e,e.testNumber),e.unit),...te(e)?{csvLabel:`${L(E(e,e.testNumber),e.unit)} [derived from ${ee(e)??ne}]`}:{},get:o=>{if(l){const p=S(o,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=o.testValues?.[e.testNumber];if(u!==void 0)return P(u,e.unit);const b=S(o,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"},csvGet:o=>{if(l){const p=S(o,e.testNumber,e);return p===void 0?"":p?"PASS":"FAIL"}const u=o.testValues?.[e.testNumber];if(u!==void 0)return P(u,void 0,"engineering");const b=S(o,e.testNumber,e);return b===void 0?"":b?"PASS":"FAIL"}}}),...V.map(e=>({label:e.label,get:e.get,csvOnly:e.csvOnly}))],$=y.filter(e=>!e.csvOnly),A=Z(a.map(e=>({label:j(e,e.testNumber),...J(e)})));A&&f.appendChild(s(i,"div",{fontSize:g.body,color:c.label,lineHeight:N.base,flexShrink:"0"},A));const D=s(i,"div",{overflow:"auto",flex:"1",minHeight:"0",minWidth:"0",border:`1px solid ${c.menuBorder}`,borderRadius:F.control,...n.maxHeight?{maxHeight:n.maxHeight}:{}}),m=i.createElement("table");m.className="wmap-dielist-table",m.setAttribute("aria-label",n.title??`Die list (${t.length} dies)`);const R=s(i,"thead"),T=s(i,"tr");for(const e of $){const l=i.createElement("th");l.className="wmap-dielist-th",l.scope="col",l.textContent=e.label,T.appendChild(l)}R.appendChild(T),m.appendChild(R);const k=s(i,"tbody");for(const e of z){const l=s(i,"tr");for(const o of $){const u=i.createElement("td");u.className="wmap-dielist-td",u.textContent=o.get(e),l.appendChild(u)}k.appendChild(l)}if(m.appendChild(k),D.appendChild(m),f.appendChild(D),C>0||x.length>0){const e=[];C>0&&e.push(`Showing the first ${d.toLocaleString()} of ${t.length.toLocaleString()} dies. The CSV export contains all ${t.length.toLocaleString()}.`),x.length>0&&e.push(`${x.length} further metadata field${x.length===1?"":"s"} not shown.`),f.appendChild(s(i,"div",{fontSize:g.body,color:c.label,lineHeight:N.base,flexShrink:"0"},e.join(" ")))}return h.addEventListener("click",()=>{const e=[y.map(l=>H(l.csvLabel??l.label)).join(",")];for(const l of t)e.push(y.map(o=>H((o.csvGet??o.get)(l))).join(","));q(e.join(`
|
|
13
13
|
`),"die-list.csv","text/csv",n.onSaveText)}),f}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type SweepSpec } from '../stats/sweep.js';
|
|
2
|
+
import { type SaveImageHandler } from './toolbar.js';
|
|
3
|
+
import type { DrilldownSource, DrilldownItem } from './chartPopulation.js';
|
|
4
|
+
export type { DrilldownSource, DrilldownItem };
|
|
5
|
+
/** What the targets can draw on — the host's chart definitions and hooks. */
|
|
6
|
+
export interface DrilldownContext {
|
|
7
|
+
sweeps?: SweepSpec[];
|
|
8
|
+
onSaveImage?: SaveImageHandler;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Open the drilldown menu for `source` at viewport point `at`.
|
|
12
|
+
*
|
|
13
|
+
* `anchor` is an on-page element of the triggering render (the map canvas, or
|
|
14
|
+
* the toolbar button) — it resolves the menu layer and the chart modal's root,
|
|
15
|
+
* and gets focus back when the menu is dismissed with Escape. Returns a close
|
|
16
|
+
* function; the menu also closes on an outside press, Escape, Tab, or when a
|
|
17
|
+
* chart is picked.
|
|
18
|
+
*/
|
|
19
|
+
export declare function openDrilldownMenu(at: {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
}, anchor: HTMLElement, source: DrilldownSource, ctx: DrilldownContext): () => void;
|
|
23
|
+
//# sourceMappingURL=drilldown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isYieldEligibleDie as b}from"../core/dies.js";import{isParametricTest as y}from"../renderer/buildWaferMap.js";import{buildSweepData as T}from"../stats/sweep.js";import{renderSweepPanel as C}from"./charts/sweep.js";import{renderHistogramPanel as E}from"./charts/histogram.js";import{renderCapabilityPanel as $}from"./charts/capability.js";import{openChartExpandModal as M,populationPhrase as w}from"./charts/chartShell.js";import{buildCheckMenuEl as I,createToolbarHelpers as x,getTooltip as N,menuLayerFor as R,wireMenuA11y as L,CLR as V,FONT as k,SPACE as F}from"./toolbar.js";const P=30,v=e=>e.items.flatMap(t=>t.dies);function O(e){return e.items.map(t=>({...t,dies:t.dies.filter(a=>b(a))}))}function A(e,t,a){return(e.testDefs??[]).filter(y).map(s=>s.testNumber).filter(s=>{let o=0;for(const i of t){const n=i.testValues?.[s];if(n!==void 0&&Number.isFinite(n)&&++o>=a)return!0}return!1})}function S(e,t,a,s,o){const n=e.ownerDocument.createElement("div");n.textContent=a,n.dataset.wmapPopulation="1",Object.assign(n.style,{color:V.label,fontSize:k.body,marginBottom:F.sm});const r=e.querySelector("[data-wmap-chart-expand]")?.parentElement;r?.parentElement===e?r.after(n):e.prepend(n),M(e,t,{anchor:s,onClosed:o})}function H(e,t,a,s){const o=v(t);let i=t.notMeasuredReason??null;return!i&&!o.some(n=>b(n))&&(i="These dies are all partial or edge-excluded, which charts leave out"),i||T(o,t.testDefs,e).series.some(r=>r.points.some(d=>d.count>0))||(i="These dies have no values for this sweep\u2019s tests"),{section:"Sweep",label:e.title,unavailable:i,open:()=>{const n=C({spec:e,dies:o,population:t.population,testDefs:t.testDefs,onSaveImage:a.onSaveImage,ownerDocument:s.ownerDocument}),r=o.filter(d=>b(d)).length;M(n.card,`${e.title} \u2014 ${w(r,o.length,t.population)}`,{anchor:s,onClosed:()=>n.destroy()})}}}function W(e,t,a){const s=O(e),o=v(e).length,i=s.flatMap(l=>l.dies),n=w(i.length,o,e.population),r=(e.testDefs??[]).filter(y),d=a.ownerDocument,f=(l,m)=>e.notMeasuredReason?e.notMeasuredReason:i.length===0?"These dies are all partial or edge-excluded, which charts leave out":A(e,i,l).length>0?null:m,u={section:"Distributions",label:"Value histogram",unavailable:f(1,"These dies have no parametric test values"),open:()=>{const l=E({title:"Value histogram",items:s,testDefs:r,selectedTestNumber:e.activeTest,onSaveImage:t.onSaveImage,ownerDocument:d});S(l.card,`Value histogram \u2014 ${n}`,`Population: ${n}`,a,()=>l.destroy())}},h={section:"Distributions",label:"Process capability",unavailable:f(2,"Needs at least 2 of these dies with values for one test"),open:()=>{const l=$({title:"Process capability",items:s,testDefs:r,selectedTestNumber:e.activeTest,onSaveImage:t.onSaveImage,ownerDocument:d}),m=i.length<P?` \xB7 fewer than ${P} dies, so each Ppk is a rough estimate`:"";S(l.card,`Process capability \u2014 ${n}`,`Population: ${n}${m}`,a,()=>l.destroy())}};return[u,h]}function B(e,t,a){return[...W(e,t,a),...(t.sweeps??[]).map(s=>H(s,e,t,a))]}const g=new WeakMap;export function openDrilldownMenu(e,t,a,s){const o=t.ownerDocument,i=o.defaultView??window;g.get(o)?.();const n=B(a,s,t),r=[];let d;for(const p of n)p.section!==d&&(r.push({section:p.section}),d=p.section),r.push({label:p.label,active:!1,action:!0,enabled:p.unavailable===null,disabledHint:p.unavailable??void 0,onClick:()=>{c(),p.open()}});const{makeMenuSection:f}=x(N(o)),u=I(new DOMRect(e.x,e.y-4,0,0),r,{makeMenuSection:f},i),h=v(a).length;u.setAttribute("role","menu"),u.setAttribute("aria-label",`Open a chart of ${w(h,h,a.population)}`),u.dataset.wmapDrilldownMenu="1",R(t).appendChild(u);const l=u.getBoundingClientRect();l.bottom>i.innerHeight-4&&(u.style.top=`${Math.max(4,e.y-l.height)}px`),l.right>i.innerWidth-4&&(u.style.left=`${Math.max(4,i.innerWidth-l.width-4)}px`);let m=!1;function c(){m||(m=!0,u.remove(),o.removeEventListener("pointerdown",D,!0),i.removeEventListener("blur",c),g.get(o)===c&&g.delete(o))}function D(p){u.contains(p.target)||c()}return o.addEventListener("pointerdown",D,!0),i.addEventListener("blur",c),L(u,t,c),g.set(o,c),c}
|
|
@@ -2,7 +2,7 @@ export const ICONS={aggr:'<svg viewBox="0 0 24 24" width="16" height="16" fill="
|
|
|
2
2
|
<rect x="3" y="3" width="4" height="18" rx="1"/>
|
|
3
3
|
<rect x="10" y="3" width="4" height="18" rx="1"/>
|
|
4
4
|
<rect x="17" y="3" width="4" height="18" rx="1"/>
|
|
5
|
-
</svg>`,download:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"/><circle cx="12" cy="13" r="3"/></svg>',downloadAll:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"/><circle cx="12" cy="13" r="3"/><circle cx="5.5" cy="4.5" r="0.8" fill="currentColor" stroke="none"/><circle cx="8" cy="4.5" r="0.8" fill="currentColor" stroke="none"/><circle cx="5.5" cy="7" r="0.8" fill="currentColor" stroke="none"/><circle cx="8" cy="7" r="0.8" fill="currentColor" stroke="none"/></svg>',expand:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>',findings:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M12 2v4"/><path d="M16 2v4"/><rect width="16" height="18" x="4" y="4" rx="2"/><path d="M8 10h6"/><path d="M8 14h8"/><path d="M8 18h5"/></svg>',flipH:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 5 5-5 5V7"/><path d="m21 7-5 5 5 5V7"/><path d="M12 20v2"/><path d="M12 14v2"/><path d="M12 8v2"/><path d="M12 2v2"/></svg>',flipV:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m17 3-5 5-5-5h10"/><path d="m17 21-5-5-5 5h10"/><path d="M4 12H2"/><path d="M10 12H8"/><path d="M16 12h-2"/><path d="M22 12h-2"/></svg>',help:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>',labels:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v16"/><path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2"/><path d="M9 20h6"/></svg>',legend:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/><path d="M14 4h7"/><path d="M14 9h7"/><path d="M14 15h7"/><path d="M14 20h7"/></svg>',logScale:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21 L5 3"/><path d="M3.5 5 L5 3 L6.5 5"/><path d="M5 21 L21 21"/><path d="M19 19.5 L21 21 L19 22.5"/><path d="M6 20 C 6 12, 10 7, 20 6"/></svg>',maximize:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>',minimize:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/></svg>',mode:`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
5
|
+
</svg>`,download:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"/><circle cx="12" cy="13" r="3"/></svg>',downloadAll:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"/><circle cx="12" cy="13" r="3"/><circle cx="5.5" cy="4.5" r="0.8" fill="currentColor" stroke="none"/><circle cx="8" cy="4.5" r="0.8" fill="currentColor" stroke="none"/><circle cx="5.5" cy="7" r="0.8" fill="currentColor" stroke="none"/><circle cx="8" cy="7" r="0.8" fill="currentColor" stroke="none"/></svg>',drilldown:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 16c3-1 4-9 7-9s3 5 6 5"/></svg>',expand:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>',findings:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M12 2v4"/><path d="M16 2v4"/><rect width="16" height="18" x="4" y="4" rx="2"/><path d="M8 10h6"/><path d="M8 14h8"/><path d="M8 18h5"/></svg>',flipH:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 5 5-5 5V7"/><path d="m21 7-5 5 5 5V7"/><path d="M12 20v2"/><path d="M12 14v2"/><path d="M12 8v2"/><path d="M12 2v2"/></svg>',flipV:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m17 3-5 5-5-5h10"/><path d="m17 21-5-5-5 5h10"/><path d="M4 12H2"/><path d="M10 12H8"/><path d="M16 12h-2"/><path d="M22 12h-2"/></svg>',help:'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>',labels:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v16"/><path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2"/><path d="M9 20h6"/></svg>',legend:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/><path d="M14 4h7"/><path d="M14 9h7"/><path d="M14 15h7"/><path d="M14 20h7"/></svg>',logScale:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21 L5 3"/><path d="M3.5 5 L5 3 L6.5 5"/><path d="M5 21 L21 21"/><path d="M19 19.5 L21 21 L19 22.5"/><path d="M6 20 C 6 12, 10 7, 20 6"/></svg>',maximize:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>',minimize:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/></svg>',mode:`<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
6
6
|
<ellipse cx="12" cy="7" rx="9" ry="4" />
|
|
7
7
|
|
|
8
8
|
<g stroke="currentColor" stroke-width="1.4" opacity="0.85">
|