@wafertools/wafermap 0.30.3 → 0.31.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 (85) hide show
  1. package/AGENTS.md +9 -19
  2. package/CHANGELOG.md +140 -0
  3. package/README.md +2 -2
  4. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +89 -6
  7. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  9. package/dist/packages/canvas-adapter/charts/scatter.d.ts +16 -2
  10. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/sweep.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  13. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  14. package/dist/packages/canvas-adapter/dieList.d.ts +1 -1
  15. package/dist/packages/canvas-adapter/exportName.d.ts +1 -12
  16. package/dist/packages/canvas-adapter/exportName.js +1 -1
  17. package/dist/packages/canvas-adapter/index.d.ts +2 -3
  18. package/dist/packages/canvas-adapter/index.js +1 -1
  19. package/dist/packages/canvas-adapter/insightsTab.d.ts +11 -1
  20. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  21. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  22. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +4 -7
  23. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  24. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +15 -30
  25. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  26. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  27. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  28. package/dist/packages/canvas-adapter/toolbar.js +1 -1
  29. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  30. package/dist/packages/canvas-adapter/userGuideHtml.js +49 -43
  31. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  32. package/dist/packages/canvas-adapter/version.js +1 -1
  33. package/dist/packages/canvas-adapter/warnings.js +1 -1
  34. package/dist/packages/core/aggregates.d.ts +0 -6
  35. package/dist/packages/core/aggregates.js +1 -1
  36. package/dist/packages/core/dies.d.ts +1 -3
  37. package/dist/packages/core/dies.js +1 -1
  38. package/dist/packages/core/index.d.ts +8 -10
  39. package/dist/packages/core/index.js +1 -1
  40. package/dist/packages/core/stdf.d.ts +9 -0
  41. package/dist/packages/core/stdf.js +1 -0
  42. package/dist/packages/core/transforms.d.ts +0 -23
  43. package/dist/packages/core/transforms.js +1 -1
  44. package/dist/packages/renderer/binColors.js +1 -1
  45. package/dist/packages/renderer/buildView.d.ts +1 -9
  46. package/dist/packages/renderer/buildView.js +1 -1
  47. package/dist/packages/renderer/buildWaferMap.d.ts +54 -34
  48. package/dist/packages/renderer/buildWaferMap.js +1 -1
  49. package/dist/packages/renderer/colorMap.d.ts +0 -2
  50. package/dist/packages/renderer/colorMap.js +1 -1
  51. package/dist/packages/renderer/deprecate.d.ts +1 -1
  52. package/dist/packages/renderer/deprecate.js +1 -1
  53. package/dist/packages/renderer/derivedTests/apply.js +1 -1
  54. package/dist/packages/renderer/derivedTests/parser.js +1 -1
  55. package/dist/packages/renderer/index.d.ts +6 -5
  56. package/dist/packages/renderer/index.js +1 -1
  57. package/dist/packages/renderer/spec.d.ts +17 -0
  58. package/dist/packages/renderer/spec.js +1 -1
  59. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  60. package/dist/packages/stats/capability.d.ts +4 -2
  61. package/dist/packages/stats/capability.js +1 -1
  62. package/dist/packages/stats/clusterDetection.d.ts +2 -1
  63. package/dist/packages/stats/correlation.d.ts +1 -1
  64. package/dist/packages/stats/facets.d.ts +1 -1
  65. package/dist/packages/stats/index.d.ts +6 -16
  66. package/dist/packages/stats/index.js +1 -1
  67. package/dist/packages/stats/mergeTestDefs.d.ts +1 -1
  68. package/dist/packages/stats/mergeTestDefs.js +1 -1
  69. package/dist/packages/stats/patternClassification.d.ts +2 -1
  70. package/dist/packages/stats/regions.d.ts +2 -1
  71. package/dist/packages/stats/renderFindingsReport.d.ts +0 -4
  72. package/dist/packages/stats/renderFindingsReport.js +1 -21
  73. package/dist/packages/stats/renderSummaryReport.js +11 -11
  74. package/dist/packages/stats/sweep.d.ts +8 -0
  75. package/dist/packages/stats/sweep.js +1 -1
  76. package/dist/packages/stats/types.d.ts +9 -3
  77. package/package.json +2 -2
  78. package/dist/packages/canvas-adapter/deprecated.d.ts +0 -7
  79. package/dist/packages/canvas-adapter/deprecated.js +0 -1
  80. package/dist/packages/core/deprecated.d.ts +0 -60
  81. package/dist/packages/core/deprecated.js +0 -1
  82. package/dist/packages/renderer/deprecated.d.ts +0 -45
  83. package/dist/packages/renderer/deprecated.js +0 -1
  84. package/dist/packages/stats/deprecated.d.ts +0 -85
  85. package/dist/packages/stats/deprecated.js +0 -1
@@ -1 +1 @@
1
- import{SHADOW as j,LEADING as H,wireControlHover as L,controlStyle as T,SPACE as m,RADIUS as E,fontPx as F,FONT as v,CLR as p,Z_BASE as U,menuLayerFor as X,wireListNavigation as Y,MENU_SEARCH_THRESHOLD as N,makeMenuSearchBox as K,markMenuTrigger as M,saveImageBlob as Z,openReparentedModal as J}from"../toolbar.js";import{ICONS as P}from"../icons.js";import{fmt as Q,fmtColorbarAxis as ee}from"../../renderer/fmt.js";import{markedTestLabel as te,DERIVED_LANE_PAD as ne}from"../../renderer/testLabel.js";export function openChartExpandModal(e,i,n={}){for(const a of e.querySelectorAll('div[style*="position: absolute"]'))a.style.display==="block"&&a.style.pointerEvents==="none"&&(a.style.display="none");const t=e.querySelector("[data-wmap-chart-expand]"),o=e.getAttribute("style")??"",r=e.dataset.wmapChartGrow??"plot";Object.assign(e.style,{flex:"1",minHeight:"0",alignSelf:"stretch",border:"none",borderRadius:"0"}),e.dataset.wmapExpanded="1";const l=J([e],{title:i,anchor:n.anchor,ownerDocument:e.ownerDocument,boxSize:oe[r],onClosed:()=>{delete e.dataset.wmapExpanded,e.setAttribute("style",o),t&&(t.style.display="flex"),n.onClosed?.()}});if(!l){delete e.dataset.wmapExpanded,e.setAttribute("style",o);return}if(t&&(t.style.display="none"),r==="rows"){const a=()=>{const s=Number(e.dataset.wmapChartFullH);if(!s)return;const c=l.box.offsetHeight-l.contentWrap.clientHeight;l.setNaturalHeight(`min(90vh, ${Math.ceil(c+s)}px)`)};a(),(e.ownerDocument.defaultView??window).requestAnimationFrame(a)}}const oe={plot:{width:"min(94vw, 1100px)",height:"min(90vh, 760px)"},rows:{width:"min(94vw, 960px)",height:"min(90vh, 700px)"},square:{width:"min(94vw, 820px)",height:"min(90vh, 860px)"}},ie={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040",accent:"#1a66cc",limitLine:"#b45309"},W={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border",accent:"icon-active",limitLine:"warn-text"};export function resolveChartCanvasColors(e){const i=getComputedStyle(e),n={};for(const t of Object.keys(W)){const o=i.getPropertyValue(`--wmap-${W[t]}`).trim();n[t]=o||ie[t]}return n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,i){const n=e.ownerDocument.defaultView??window;let t=0;const o=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,i()}))});return o.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,o.disconnect()}}}export function growCardToFitContent(e,i,n){const t=`${R(e,i)+n}px`;e.style.minHeight!==t&&(e.style.minHeight=t)}function R(e,i){const n=e.ownerDocument.defaultView??window,t=n.getComputedStyle(e);let o=0;for(const r of Array.from(e.children)){if(r===i)continue;const l=r,a=n.getComputedStyle(l);a.display==="none"||a.position==="absolute"||a.position==="fixed"||(o+=l.offsetHeight+(parseFloat(a.marginTop)||0)+(parseFloat(a.marginBottom)||0))}return t.boxSizing==="border-box"&&(o+=(parseFloat(t.paddingTop)||0)+(parseFloat(t.paddingBottom)||0)+(parseFloat(t.borderTopWidth)||0)+(parseFloat(t.borderBottomWidth)||0)),o}function _(e,i){const n=e.ownerDocument.defaultView??window;let t=0;for(const o of Array.from(e.children)){if(o===i)continue;const r=o,l=n.getComputedStyle(r);l.display==="none"||l.position==="absolute"||l.position==="fixed"||(t+=r.offsetHeight+(parseFloat(l.marginTop)||0)+(parseFloat(l.marginBottom)||0))}return t}export const WAFER_MENU_HINT="<br><em>right-click to chart this wafer</em>";export function populationPhrase(e,i,n){const t=e.toLocaleString(),o=e===1?"die":"dies";return n?e===i?`${t} ${o} ${n}`:`${t} of ${i.toLocaleString()} dies ${n} (partial and edge-excluded dies left out)`:`${t} ${o}`}export function setChartGrow(e,i){e.dataset.wmapChartGrow=i}export function isExpandedCard(e){return e.dataset.wmapExpanded==="1"}export function bodyRoom(e,i,n){const o=(e.ownerDocument.defaultView??window).getComputedStyle(e),r=e.clientHeight-(parseFloat(o.paddingTop)||0)-(parseFloat(o.paddingBottom)||0),l=o.boxSizing==="border-box"?(parseFloat(o.paddingTop)||0)+(parseFloat(o.paddingBottom)||0)+(parseFloat(o.borderTopWidth)||0)+(parseFloat(o.borderBottomWidth)||0):0;return r-(R(e,i)-l)-(n?_(i,n):0)}export function fitRowsHeight(e,i,n,t,o=0){if(growCardToFitContent(e,i,o+n),e.dataset.wmapChartFullH=String(Math.ceil(R(e,i)+o+t)),!isExpandedCard(e))return n;const r=bodyRoom(e,i)-o;return Math.max(n,Math.min(t,Math.floor(r)))}export function chartFillHeight(e,i,n,t){const o=_(i,n);return growCardToFitContent(e,i,o+t),Math.max(t,i.clientHeight-o)}export function chartDpr(e){return(e.ownerDocument.defaultView??window).devicePixelRatio||1}export function prepareCanvas(e,i,n,t){const o=chartDpr(e);e.width=Math.max(1,Math.floor(n*o)),e.height=Math.max(1,Math.floor(t*o)),e.style.width=`${n}px`,e.style.height=`${t}px`;const r=e.getContext("2d");return r?(r.setTransform(o,0,0,o,0,0),r.clearRect(0,0,n,t),r.font=`${F(-1)}px system-ui, sans-serif`,r.textBaseline="middle",{ctx:r,theme:resolveChartCanvasColors(i)}):null}export function applyCanvasFlow(e,i=0){let n;if(typeof i=="number")n=i;else{const t=(i.ownerDocument.defaultView??window).getComputedStyle(i);n=i.offsetTop+i.offsetHeight+(parseFloat(t.marginBottom)||0)}e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${n}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:Q(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,i,n){if(!i&&n===void 0)return{tick:formatValue,tickWithUnit:formatValue,unitLabel:""};const{tickFmt:t,axisLabel:o,scaledUnit:r}=ee(e,null,i,"engineering",n);return{tick:t,tickWithUnit:r?l=>`${t(l)} ${r}`:t,unitLabel:o}}const re=8;export const VERTICAL_TICK_SPACING_PX=28;export function horizontalTickSpacing(e,i,n){return(t,o)=>{const r=makeAxisFormat(i,n,t).tick;return o.reduce((l,a)=>Math.max(l,e.measureText(r(a)).width),0)+re}}export function drawAxisUnit(e,i,n,t,o){const r={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font=`${F(-1)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="middle",e.fillStyle=o,e.fillText(`(${i})`,n,t),Object.assign(e,r)}export function saveCanvasPng(e,i,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const o=t.getContext("2d");o.fillStyle="#ffffff",o.fillRect(0,0,t.width,t.height),o.drawImage(e,0,0),t.toBlob(r=>{r&&Z(r,i,n)},"image/png")}const z="10px";export function chartSwatchCss(e,i="solid"){return["display:inline-block",`width:${z}`,`height:${z}`,`border-radius:${E.pill}`,`background:${i==="outline"?"transparent":e}`,`border:1px ${i==="outline"?"dashed":"solid"} ${e}`,"box-sizing:border-box","flex-shrink:0"].join(";")}const le="0.35";export function makeSeriesLegendItem(e,i,n=document){const t=n.createElement("button");t.type="button",Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:m.xs,padding:`${m.xs} ${m.md}`,borderRadius:E.pill,border:`1px solid ${p.menuBorder}`,background:"none",cursor:"pointer",fontSize:v.body,color:p.text,whiteSpace:"nowrap"}),L(t,"bare");const o=n.createElement("span");return o.style.cssText=chartSwatchCss(i),t.append(o,n.createTextNode(e)),{el:t,setState({selected:r,dimmed:l}){t.style.borderColor=r?p.iconActive:p.menuBorder,t.style.fontWeight=r?"600":"400",t.style.opacity=l?le:"1",t.setAttribute("aria-pressed",r?"true":"false")}}}export function cardFrameStyle(){return{background:p.menuBg,border:`1px solid ${p.menuBorder}`,borderRadius:E.container,padding:m.xl,fontVariantNumeric:"tabular-nums"}}export function cardShell(e,i,n=document){const t=n.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",...cardFrameStyle(),minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const o=t.ownerDocument.createElement("div");Object.assign(o.style,{display:"flex",alignItems:"center",gap:m.sm,paddingBottom:"6px",marginBottom:m.md,borderBottom:`1px solid ${p.menuBorder}`});const r=t.ownerDocument.createElement("div");r.textContent=e,Object.assign(r.style,{color:p.value,fontSize:v.heading,fontWeight:"600",flex:"1"}),o.appendChild(r);const l=makeTooltip(t),a=t.ownerDocument.createElement("button");a.type="button",a.setAttribute("aria-label","Save as PNG"),a.innerHTML=P.download,Object.assign(a.style,{...T("outlined"),color:p.label,padding:"0",width:"22px",height:"22px",lineHeight:H.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(a),attachChartTip(a,t,l,"Save as PNG"),a.addEventListener("click",()=>{const f=t.querySelector("canvas");f&&saveCanvasPng(f,e,i)}),o.appendChild(a);const s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Expand"),s.innerHTML=P.expand,Object.assign(s.style,{...T("outlined"),color:p.label,padding:"0",width:"22px",height:"22px",lineHeight:H.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(s),attachChartTip(s,t,l,"Expand"),s.dataset.wmapChartExpand="1",s.addEventListener("click",()=>openChartExpandModal(t,r.textContent??e)),o.appendChild(s),t.appendChild(o);const c=t.ownerDocument.createElement("div");Object.assign(c.style,{display:"flex",gap:m.sm,marginBottom:m.md,flexWrap:"wrap",alignItems:"center"}),t.appendChild(c);const d=t.ownerDocument.createElement("div");return Object.assign(d.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(d),{card:t,heading:r,controlsRow:c,body:d}}export function makeSegmented(e,i,n,t=document,o=!1){const r=t.createElement("div");r.setAttribute("role","radiogroup"),Object.assign(r.style,{display:"inline-flex",border:`1px solid ${p.menuBorder}`,borderRadius:E.control,overflow:"hidden"});const l=`seg-${Math.random().toString(36).slice(2,9)}`,a=[];return e.forEach(([s,c],d)=>{const f=t.createElement("label");Object.assign(f.style,{display:"inline-flex",alignItems:"center",fontSize:v.body,padding:o?"1px 6px":"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:d>0?`1px solid ${p.menuBorder}`:"none"}),L(f,"bare");const u=t.createElement("input");u.type="radio",u.name=l,u.value=s,u.checked=s===i,Object.assign(u.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const w=()=>{f.style.background=u.checked?p.bgActive:p.menuBg,f.style.color=u.checked?p.iconActive:p.text,f.dataset.on=u.checked?"true":"false"};a.push(w),w(),u.addEventListener("change",()=>{if(u.checked){for(const C of a)C();n(s)}}),f.append(u,t.createTextNode(c)),r.appendChild(f)}),r}export function makeBackButton(e,i=document){const n=i.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{...T("outlined"),background:p.menuBg,color:p.label}),L(n),n.addEventListener("click",e),n}export function renderEmptyState(e,i,n){const t=e.ownerDocument.createElement("div");t.textContent=i,Object.assign(t.style,{color:p.label,fontSize:v.body,padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeListSelect(e,i,n,t={}){const{maxWidth:o="200px",ownerDocument:r=document,ariaLabel:l,emptyText:a,searchPlaceholder:s="Filter\u2026",hook:c}=t;let d=i;const f=b=>e.find($=>$.value===b)?.label??"",u=r.createElement("button");u.type="button",c&&(u.dataset.wmapSelect=c),l&&u.setAttribute("aria-label",l),Object.assign(u.style,{fontSize:v.body,padding:`${m.xxs} ${m.sm}`,background:p.menuBg,color:p.text,border:`1px solid ${p.menuBorder}`,borderRadius:E.control,maxWidth:o,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:m.sm,justifyContent:"space-between"}),L(u),M(u,!1);const w=r.createElement("span");Object.assign(w.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const C=r.createElement("span");C.textContent="\u25BE",C.style.flex="0 0 auto",u.append(w,C);const S=()=>{w.textContent=e.length===0?a??"(none)":f(d)||a||"(none)"};if(S(),e.length===0)return u.disabled=!0,u.style.opacity="0.6",u.style.cursor="default",Object.defineProperty(u,"value",{get:()=>d,set:b=>{d=b,S()}});let x=null;const k=()=>{x&&(x.remove(),x=null,M(u,!1))},A=()=>{k(),u.focus()};function V(){const b=u.getBoundingClientRect(),$=r.defaultView??window,D=Math.max(b.width,220),G=Math.min(b.left,Math.max(4,($.innerWidth??1/0)-D-4));x=r.createElement("div"),x.setAttribute("role","listbox"),x.setAttribute("aria-label",l??"Options"),Object.assign(x.style,{position:"fixed",top:`${b.bottom+4}px`,left:`${G}px`,background:p.menuBg,border:`1px solid ${p.menuBorder}`,borderRadius:E.control,boxShadow:j.menu,zIndex:U,minWidth:`${D}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const B=[],O=()=>B.filter(h=>h.row.style.display!=="none").map(h=>h.row);if(e.length>N){const h=K(y=>{for(const g of B)g.row.style.display=g.label.includes(y)?"":"none"},s,r);Object.assign(h.style,{position:"sticky",top:"0",zIndex:"1",background:p.menuBg}),h.addEventListener("keydown",y=>{if(y.key==="Escape"){A();return}y.key==="ArrowDown"&&(y.preventDefault(),O()[0]?.focus())}),x.appendChild(h)}for(const h of e){const y=h.value===d,g=r.createElement("div");g.textContent=h.label,g.setAttribute("role","option"),g.setAttribute("aria-selected",y?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:`${m.sm} ${m.xl}`,fontSize:v.body,cursor:"pointer",color:y?p.iconActive:p.text,fontWeight:y?"700":"400",background:y?p.menuActive:"transparent",whiteSpace:"nowrap"}),g.addEventListener("mouseenter",()=>{y||(g.style.background=p.menuHover)}),g.addEventListener("mouseleave",()=>{y||(g.style.background="transparent")}),g.addEventListener("click",q=>{q.stopPropagation(),d=h.value,S(),A(),n(h.value)}),x.appendChild(g),B.push({row:g,label:h.label.toLowerCase()})}if(Y(x,O,A),X(u).appendChild(x),M(u,!0),e.length<=N){const h=e.findIndex(y=>y.value===d);O()[h>=0?h:0]?.focus()}}u.addEventListener("click",b=>{if(b.stopPropagation(),x){k();return}V()});const I=b=>{if(!u.isConnected){r.removeEventListener("click",I);return}x&&!x.contains(b.target)&&!u.contains(b.target)&&k()};return r.addEventListener("click",I),Object.defineProperty(u,"value",{get:()=>d,set:b=>{d=b,S()}})}export function testOptionLabels(e){const i=e.some(n=>n.derived);return e.map(n=>(i&&!n.derived?ne:"")+te(n,n.testNumber))}export function makeTestSelect(e,i,n,t={}){const{maxWidth:o="200px",emptyText:r="No parametric tests",ownerDocument:l=document}=t;return makeListSelect(testOptionLabels(e).map((a,s)=>({value:String(e[s].testNumber),label:a})),i!==null?String(i):"",a=>n(Number(a)),{maxWidth:o,ownerDocument:l,ariaLabel:"Test",emptyText:r,searchPlaceholder:"Filter tests\u2026"})}export function makeLinkedAxisPrefs(e,i,n,t=document){let o=i?.includeLimits,r=i?.clipOutliers??!1;const l=()=>n({includeLimits:o,clipOutliers:r});return{get:()=>({includeLimits:o,clipOutliers:r}),sync(a,s){e.innerHTML="",s&&e.appendChild(makeToggle("Axis includes limits",a,c=>{o=c,l()},t)),e.appendChild(makeToggle("Clip outliers",r,c=>{r=c,l()},t))},set(a){return a.includeLimits===o&&a.clipOutliers===r?!1:(o=a.includeLimits,r=a.clipOutliers,!0)}}}export function makeLinkedTestSelect(e,i,n,t={}){let o=i;const r=makeTestSelect(e,o,l=>{l!==o&&(o=l,n(l))},t);return{el:r,get:()=>o,set(l){return o===l||!e.some(a=>a.testNumber===l)?!1:(o=l,r.value=String(l),!0)}}}export const ALL_GROUPS="\0all";export function makeLinkedGroupSelect(e,i,n,t,o={}){let r=n;const l=d=>d??ALL_GROUPS,a=d=>d===ALL_GROUPS?null:d,s=makeLabeledSelect(i,[{value:ALL_GROUPS,label:"All groups"},...e.map(d=>({value:d,label:d}))],l(r),d=>{const f=a(d);f!==r&&(r=f,t(f))},{...o,hook:"group-scope"}),c=s.querySelector('[data-wmap-select="group-scope"]');return{el:s,get:()=>r,set(d){return r===d||d!==null&&!e.includes(d)?!1:(r=d,c&&(c.value=l(d)),!0)}}}export function makeWaferSelect(e,i,n,t={}){const{maxWidth:o="160px",allLabel:r="All wafers",ownerDocument:l=document}=t,a="\0all";return makeListSelect([{value:a,label:r},...e.map((s,c)=>({value:String(c),label:s.label??`#${c}`}))],i===null?a:String(i),s=>n(s===a?null:Number(s)),{maxWidth:o,ownerDocument:l,ariaLabel:r,searchPlaceholder:"Filter wafers\u2026"})}export function makeToggle(e,i,n,t=document){const o=t.createElement("label");Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:m.xs,fontSize:v.body,color:p.label,cursor:"pointer",userSelect:"none"}),L(o,"bare");const r=t.createElement("input");return r.type="checkbox",r.checked=i,r.style.cssText="margin:0;cursor:pointer;",r.addEventListener("change",()=>n(r.checked)),o.append(r,t.createTextNode(e)),o}export function makeLabeledSelect(e,i,n,t,o={}){const{maxWidth:r="160px",hook:l,ownerDocument:a=document}=o,s=a.createElement("label");s.textContent=e,Object.assign(s.style,{color:p.label,fontSize:v.body,display:"flex",alignItems:"center",gap:m.xs});const c=makeListSelect(i,n,t,{maxWidth:r,ownerDocument:a,hook:l,ariaLabel:e.replace(/:\s*$/,"")});return s.appendChild(c),s}export function makeChartGridWrap(e=document){const i=e.createElement("div");return i.dataset.wmapChartGrid="1",Object.assign(i.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:m.lg,flex:"0 0 auto"}),i}export function makeTooltip(e){const i=e.ownerDocument.createElement("div");return Object.assign(i.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:"rgba(30, 32, 40, 0.93)",color:"#f0f0f2",border:"1px solid rgba(255,255,255,0.10)",borderRadius:E.control,padding:`${m.sm} ${m.xl}`,fontSize:v.sub,fontFamily:v.family,maxWidth:"320px",whiteSpace:"normal",overflowWrap:"break-word",boxShadow:j.menu}),e.appendChild(i),i}export function attachChartTip(e,i,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",o=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,i,o.clientX,o.clientY)}),e.addEventListener("mousemove",o=>{n.style.display==="block"&&positionChartTooltip(n,i,o.clientX,o.clientY)}),e.addEventListener("mouseleave",()=>{n.style.display="none"}),e.addEventListener("click",()=>{n.style.display="none"}),e.addEventListener("focus",()=>{n.textContent=t,n.style.display="block";const o=e.getBoundingClientRect();positionChartTooltip(n,i,o.left+o.width/2,o.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,i,n,t){const o=i.getBoundingClientRect(),r=14,l=4;e.style.left="0",e.style.top="0";const a=e.offsetWidth,s=e.offsetHeight;let c=n-o.left+r,d=t-o.top+r;c+a+l>o.width&&(c=n-o.left-r-a),d+s+l>o.height&&(d=t-o.top-r-s),c=Math.max(l,c),d=Math.max(l,d),e.style.left=`${c}px`,e.style.top=`${d}px`}export function robustFence(e,i=1.5){const n=Float64Array.from(e.filter(a=>Number.isFinite(a)));if(n.sort(),n.length<8)return null;const t=a=>{const s=(n.length-1)*a,c=Math.floor(s),d=Math.ceil(s);return c===d?n[c]:n[c]+(n[d]-n[c])*(s-c)},o=t(.25),r=t(.75),l=r-o;return l===0?null:{lo:o-i*l,hi:r+i*l}}export function shouldIncludeLimitsByDefault(e,i,n,t,o=1/3){if(n===void 0&&t===void 0||!Number.isFinite(e)||!Number.isFinite(i))return!1;const r=Math.min(e,n??e),a=Math.max(i,t??i)-r;if(a<=0)return!0;const s=i-e;return s===0?!0:s/a>=o}export function resolveAxisRange(e){const{dataMin:i,dataMax:n,limitLow:t,limitHigh:o,includeLimits:r,clipOutliers:l,values:a}=e;let s=i,c=n,d=0;if(l&&a?.length){const u=robustFence(a);if(u){const w=Math.max(s,u.lo),C=Math.min(c,u.hi);C>w&&(d=a.filter(S=>Number.isFinite(S)&&(S<w||S>C)).length,s=w,c=C)}}r&&(t!==void 0&&(s=Math.min(s,t)),o!==void 0&&(c=Math.max(c,o))),s===c&&(s-=1,c+=1);const f=[];return t!==void 0&&t<s&&f.push({value:t,label:"LSL",side:"lo"}),t!==void 0&&t>c&&f.push({value:t,label:"LSL",side:"hi"}),o!==void 0&&o>c&&f.push({value:o,label:"USL",side:"hi"}),o!==void 0&&o<s&&f.push({value:o,label:"USL",side:"lo"}),{lo:s,hi:c,offAxis:f,clippedCount:d}}export function limitLabelSide(e,i,n,t,o,r=3){const l=o?-1:1;return(l<0?e-r-i>=n:e+r+i<=t)?l:-l}export function drawOffAxisLimits(e,i,n,t,o,r){if(i.length){e.save(),e.fillStyle=o,e.font=`${F(-1)}px system-ui, sans-serif`;for(const{value:l,label:a,side:s}of i){const c=t==="horizontal"?s==="lo"?"\u2190":"\u2192":s==="lo"?"\u2193":"\u2191",d=s==="lo"?`${c} ${a} ${r(l)}`:`${a} ${r(l)} ${c}`;t==="horizontal"?(e.textBaseline="top",e.textAlign=s==="lo"?"left":"right",e.fillText(d,s==="lo"?n.left+2:n.right-2,n.top+2)):(e.textBaseline=s==="lo"?"bottom":"top",e.textAlign="left",e.fillText(d,n.left+4,s==="lo"?n.bottom-2:n.top+2))}e.restore()}}
1
+ import{SHADOW as D,LEADING as j,wireControlHover as L,controlStyle as I,SPACE as m,RADIUS as C,fontPx as k,FONT as S,CLR as f,Z_BASE as X,menuLayerFor as K,wireListNavigation as Y,MENU_SEARCH_THRESHOLD as W,makeMenuSearchBox as Z,markMenuTrigger as R,saveImageBlob as J,openReparentedModal as Q}from"../toolbar.js";import{ICONS as _}from"../icons.js";import{minOf as ee,maxOf as te}from"../../core/utils.js";import{fmt as ne,fmtColorbarAxis as oe}from"../../renderer/fmt.js";import{markedTestLabel as ie,DERIVED_LANE_PAD as re}from"../../renderer/testLabel.js";export function openChartExpandModal(e,o,n={}){for(const s of e.querySelectorAll('div[style*="position: absolute"]'))s.style.display==="block"&&s.style.pointerEvents==="none"&&(s.style.display="none");const t=e.querySelector("[data-wmap-chart-expand]"),i=e.getAttribute("style")??"",r=e.dataset.wmapChartGrow??"plot";Object.assign(e.style,{flex:"1",minHeight:"0",alignSelf:"stretch",border:"none",borderRadius:"0"}),e.dataset.wmapExpanded="1";const l=Q([e],{title:o,anchor:n.anchor,ownerDocument:e.ownerDocument,boxSize:le[r],onClosed:()=>{delete e.dataset.wmapExpanded,e.setAttribute("style",i),t&&(t.style.display="flex"),n.onClosed?.()}});if(!l){delete e.dataset.wmapExpanded,e.setAttribute("style",i);return}if(t&&(t.style.display="none"),r==="rows"){const s=()=>{const a=Number(e.dataset.wmapChartFullH);if(!a)return;const d=l.box.offsetHeight-l.contentWrap.clientHeight;l.setNaturalHeight(`min(90vh, ${Math.ceil(d+a)}px)`)};s(),(e.ownerDocument.defaultView??window).requestAnimationFrame(s)}}const le={plot:{width:"min(94vw, 1100px)",height:"min(90vh, 760px)"},rows:{width:"min(94vw, 960px)",height:"min(90vh, 700px)"},square:{width:"min(94vw, 820px)",height:"min(90vh, 860px)"}};export const GRID_ALPHA=.4;export function withAlpha(e,o){const n=e.trim();let t,i,r,l=1;const s=n.match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i),a=n.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i);if(s){const d=s[1].length===3?s[1].split("").map(c=>c+c).join(""):s[1];[t,i,r]=[0,2,4].map(c=>parseInt(d.slice(c,c+2),16)),d.length===8&&(l=parseInt(d.slice(6,8),16)/255)}else if(a)[t,i,r]=[a[1],a[2],a[3]].map(Number),a[4]!==void 0&&(l=a[4].endsWith("%")?parseFloat(a[4])/100:Number(a[4]));else return e;return`rgba(${t}, ${i}, ${r}, ${+(l*o).toFixed(3)})`}const se={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040",accent:"#1a66cc",limitLine:"#b45309"},P={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border",accent:"icon-active",limitLine:"warn-text"};export function resolveChartCanvasColors(e){const o=getComputedStyle(e),n={};for(const t of Object.keys(P)){const i=o.getPropertyValue(`--wmap-${P[t]}`).trim();n[t]=i||se[t]}return n.grid=withAlpha(n.border,GRID_ALPHA),n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,o){const n=e.ownerDocument.defaultView??window;let t=0;const i=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,o()}))});return i.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,i.disconnect()}}}export function growCardToFitContent(e,o,n){const t=`${M(e,o)+n}px`;e.style.minHeight!==t&&(e.style.minHeight=t)}function M(e,o){const n=e.ownerDocument.defaultView??window,t=n.getComputedStyle(e);let i=0;for(const r of Array.from(e.children)){if(r===o)continue;const l=r,s=n.getComputedStyle(l);s.display==="none"||s.position==="absolute"||s.position==="fixed"||(i+=l.offsetHeight+(parseFloat(s.marginTop)||0)+(parseFloat(s.marginBottom)||0))}return t.boxSizing==="border-box"&&(i+=(parseFloat(t.paddingTop)||0)+(parseFloat(t.paddingBottom)||0)+(parseFloat(t.borderTopWidth)||0)+(parseFloat(t.borderBottomWidth)||0)),i}function z(e,o){const n=e.ownerDocument.defaultView??window;let t=0;for(const i of Array.from(e.children)){if(i===o)continue;const r=i,l=n.getComputedStyle(r);l.display==="none"||l.position==="absolute"||l.position==="fixed"||(t+=r.offsetHeight+(parseFloat(l.marginTop)||0)+(parseFloat(l.marginBottom)||0))}return t}export const WAFER_MENU_HINT="<br><em>right-click to chart this wafer</em>";export function populationPhrase(e,o,n){const t=e.toLocaleString(),i=e===1?"die":"dies";return n?e===o?`${t} ${i} ${n}`:`${t} of ${o.toLocaleString()} dies ${n} (partial and edge-excluded dies left out)`:`${t} ${i}`}export function setChartGrow(e,o){e.dataset.wmapChartGrow=o}export function isExpandedCard(e){return e.dataset.wmapExpanded==="1"}export function bodyRoom(e,o,n){const i=(e.ownerDocument.defaultView??window).getComputedStyle(e),r=e.clientHeight-(parseFloat(i.paddingTop)||0)-(parseFloat(i.paddingBottom)||0),l=i.boxSizing==="border-box"?(parseFloat(i.paddingTop)||0)+(parseFloat(i.paddingBottom)||0)+(parseFloat(i.borderTopWidth)||0)+(parseFloat(i.borderBottomWidth)||0):0;return r-(M(e,o)-l)-(n?z(o,n):0)}export function fitRowsHeight(e,o,n,t,i=0){if(growCardToFitContent(e,o,i+n),e.dataset.wmapChartFullH=String(Math.ceil(M(e,o)+i+t)),!isExpandedCard(e))return n;const r=bodyRoom(e,o)-i;return Math.max(n,Math.min(t,Math.floor(r)))}export function chartFillHeight(e,o,n,t){const i=z(o,n);return growCardToFitContent(e,o,i+t),Math.max(t,o.clientHeight-i)}export function chartDpr(e){return(e.ownerDocument.defaultView??window).devicePixelRatio||1}export function prepareCanvas(e,o,n,t){const i=chartDpr(e);e.width=Math.max(1,Math.floor(n*i)),e.height=Math.max(1,Math.floor(t*i)),e.style.width=`${n}px`,e.style.height=`${t}px`;const r=e.getContext("2d");return r?(r.setTransform(i,0,0,i,0,0),r.clearRect(0,0,n,t),r.font=`${k(-1)}px system-ui, sans-serif`,r.textBaseline="middle",{ctx:r,theme:resolveChartCanvasColors(o)}):null}export function applyCanvasFlow(e,o=0){let n;if(typeof o=="number")n=o;else{const t=(o.ownerDocument.defaultView??window).getComputedStyle(o);n=o.offsetTop+o.offsetHeight+(parseFloat(t.marginBottom)||0)}e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${n}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:ne(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,o,n){if(!o&&n===void 0)return{tick:formatValue,tickWithUnit:formatValue,unitLabel:""};const{tickFmt:t,axisLabel:i,scaledUnit:r}=oe(e,null,o,"engineering",n);return{tick:t,tickWithUnit:r?l=>`${t(l)} ${r}`:t,unitLabel:i}}const ae=8;export const VERTICAL_TICK_SPACING_PX=28;export function horizontalTickSpacing(e,o,n){return(t,i)=>{const r=makeAxisFormat(o,n,t).tick;return i.reduce((l,s)=>Math.max(l,e.measureText(r(s)).width),0)+ae}}export function drawAxisUnit(e,o,n,t,i){const r={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font=`${k(-1)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="middle",e.fillStyle=i,e.fillText(`(${o})`,n,t),Object.assign(e,r)}export function saveCanvasPng(e,o,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const i=t.getContext("2d");i.fillStyle="#ffffff",i.fillRect(0,0,t.width,t.height),i.drawImage(e,0,0),t.toBlob(r=>{r&&J(r,o,n)},"image/png")}const V="10px";export function chartSwatchCss(e,o="solid"){return["display:inline-block",`width:${V}`,`height:${V}`,`border-radius:${C.pill}`,`background:${o==="outline"?"transparent":e}`,`border:1px ${o==="outline"?"dashed":"solid"} ${e}`,"box-sizing:border-box","flex-shrink:0"].join(";")}const ce="0.35";export function makeSeriesLegendItem(e,o,n=document){const t=n.createElement("button");t.type="button",Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:m.xs,padding:`${m.xs} ${m.md}`,borderRadius:C.pill,border:`1px solid ${f.menuBorder}`,background:"none",cursor:"pointer",fontSize:S.body,color:f.text,whiteSpace:"nowrap"}),L(t,"bare");const i=n.createElement("span");return i.style.cssText=chartSwatchCss(o),t.append(i,n.createTextNode(e)),{el:t,setState({selected:r,dimmed:l}){t.style.borderColor=r?f.iconActive:f.menuBorder,t.style.fontWeight=r?"600":"400",t.style.opacity=l?ce:"1",t.setAttribute("aria-pressed",r?"true":"false")}}}export function cardFrameStyle(){return{background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:C.container,padding:m.xl,fontVariantNumeric:"tabular-nums"}}export function cardShell(e,o,n=document){const t=n.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",...cardFrameStyle(),minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const i=t.ownerDocument.createElement("div");Object.assign(i.style,{display:"flex",alignItems:"center",gap:m.sm,paddingBottom:"6px",marginBottom:m.md,borderBottom:`1px solid ${f.menuBorder}`});const r=t.ownerDocument.createElement("div");r.textContent=e,Object.assign(r.style,{color:f.value,fontSize:S.heading,fontWeight:"600",flex:"1"}),i.appendChild(r);const l=makeTooltip(t),s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Save as PNG"),s.innerHTML=_.download,Object.assign(s.style,{...I("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:j.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(s),attachChartTip(s,t,l,"Save as PNG"),s.addEventListener("click",()=>{const u=t.querySelector("canvas");u&&saveCanvasPng(u,e,o)}),i.appendChild(s);const a=t.ownerDocument.createElement("button");a.type="button",a.setAttribute("aria-label","Expand"),a.innerHTML=_.expand,Object.assign(a.style,{...I("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:j.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(a),attachChartTip(a,t,l,"Expand"),a.dataset.wmapChartExpand="1",a.addEventListener("click",()=>openChartExpandModal(t,r.textContent??e)),i.appendChild(a),t.appendChild(i);const d=t.ownerDocument.createElement("div");Object.assign(d.style,{display:"flex",gap:m.sm,marginBottom:m.md,flexWrap:"wrap",alignItems:"center"}),t.appendChild(d);const c=t.ownerDocument.createElement("div");return Object.assign(c.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(c),{card:t,heading:r,controlsRow:d,body:c}}export function makeSegmented(e,o,n,t=document,i=!1){const r=t.createElement("div");r.setAttribute("role","radiogroup"),Object.assign(r.style,{display:"inline-flex",border:`1px solid ${f.menuBorder}`,borderRadius:C.control,overflow:"hidden"});const l=`seg-${Math.random().toString(36).slice(2,9)}`,s=[];return e.forEach(([a,d],c)=>{const u=t.createElement("label");Object.assign(u.style,{display:"inline-flex",alignItems:"center",fontSize:S.body,padding:i?"1px 6px":"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:c>0?`1px solid ${f.menuBorder}`:"none"}),L(u,"bare");const p=t.createElement("input");p.type="radio",p.name=l,p.value=a,p.checked=a===o,Object.assign(p.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const w=()=>{u.style.background=p.checked?f.bgActive:f.menuBg,u.style.color=p.checked?f.iconActive:f.text,u.dataset.on=p.checked?"true":"false"};s.push(w),w(),p.addEventListener("change",()=>{if(p.checked){for(const v of s)v();n(a)}}),u.append(p,t.createTextNode(d)),r.appendChild(u)}),r}export function makeBackButton(e,o=document){const n=o.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{...I("outlined"),background:f.menuBg,color:f.label}),L(n),n.addEventListener("click",e),n}export function renderEmptyState(e,o,n){const t=e.ownerDocument.createElement("div");t.textContent=o,Object.assign(t.style,{color:f.label,fontSize:S.body,padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeListSelect(e,o,n,t={}){const{maxWidth:i="200px",ownerDocument:r=document,ariaLabel:l,emptyText:s,searchPlaceholder:a="Filter\u2026",hook:d}=t;let c=o;const u=x=>e.find(O=>O.value===x)?.label??"",p=r.createElement("button");p.type="button",d&&(p.dataset.wmapSelect=d),l&&p.setAttribute("aria-label",l),Object.assign(p.style,{fontSize:S.body,padding:`${m.xxs} ${m.sm}`,background:f.menuBg,color:f.text,border:`1px solid ${f.menuBorder}`,borderRadius:C.control,maxWidth:i,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:m.sm,justifyContent:"space-between"}),L(p),R(p,!1);const w=r.createElement("span");Object.assign(w.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const v=r.createElement("span");v.textContent="\u25BE",v.style.flex="0 0 auto",p.append(w,v);const E=()=>{w.textContent=e.length===0?s??"(none)":u(c)||s||"(none)"};if(E(),e.length===0)return p.disabled=!0,p.style.opacity="0.6",p.style.cursor="default",Object.defineProperty(p,"value",{get:()=>c,set:x=>{c=x,E()}});let b=null;const B=()=>{b&&(b.remove(),b=null,R(p,!1))},$=()=>{B(),p.focus()};function G(){const x=p.getBoundingClientRect(),O=r.defaultView??window,N=Math.max(x.width,220),q=Math.min(x.left,Math.max(4,(O.innerWidth??1/0)-N-4));b=r.createElement("div"),b.setAttribute("role","listbox"),b.setAttribute("aria-label",l??"Options"),Object.assign(b.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${q}px`,background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:C.control,boxShadow:D.menu,zIndex:X,minWidth:`${N}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const T=[],F=()=>T.filter(h=>h.row.style.display!=="none").map(h=>h.row);if(e.length>W){const h=Z(y=>{for(const g of T)g.row.style.display=g.label.includes(y)?"":"none"},a,r);Object.assign(h.style,{position:"sticky",top:"0",zIndex:"1",background:f.menuBg}),h.addEventListener("keydown",y=>{if(y.key==="Escape"){$();return}y.key==="ArrowDown"&&(y.preventDefault(),F()[0]?.focus())}),b.appendChild(h)}for(const h of e){const y=h.value===c,g=r.createElement("div");g.textContent=h.label,g.setAttribute("role","option"),g.setAttribute("aria-selected",y?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:`${m.sm} ${m.xl}`,fontSize:S.body,cursor:"pointer",color:y?f.iconActive:f.text,fontWeight:y?"700":"400",background:y?f.menuActive:"transparent",whiteSpace:"nowrap"}),g.addEventListener("mouseenter",()=>{y||(g.style.background=f.menuHover)}),g.addEventListener("mouseleave",()=>{y||(g.style.background="transparent")}),g.addEventListener("click",U=>{U.stopPropagation(),c=h.value,E(),$(),n(h.value)}),b.appendChild(g),T.push({row:g,label:h.label.toLowerCase()})}if(Y(b,F,$),K(p).appendChild(b),R(p,!0),e.length<=W){const h=e.findIndex(y=>y.value===c);F()[h>=0?h:0]?.focus()}}p.addEventListener("click",x=>{if(x.stopPropagation(),b){B();return}G()});const H=x=>{if(!p.isConnected){r.removeEventListener("click",H);return}b&&!b.contains(x.target)&&!p.contains(x.target)&&B()};return r.addEventListener("click",H),Object.defineProperty(p,"value",{get:()=>c,set:x=>{c=x,E()}})}export function testOptionLabels(e){const o=e.some(n=>n.derived);return e.map(n=>(o&&!n.derived?re:"")+ie(n,n.testNumber))}export function makeTestSelect(e,o,n,t={}){const{maxWidth:i="200px",emptyText:r="No parametric tests",ownerDocument:l=document}=t;return makeListSelect(testOptionLabels(e).map((s,a)=>({value:String(e[a].testNumber),label:s})),o!==null?String(o):"",s=>n(Number(s)),{maxWidth:i,ownerDocument:l,ariaLabel:"Test",emptyText:r,searchPlaceholder:"Filter tests\u2026"})}export function makeLinkedAxisPrefs(e,o,n,t=document){let i=o?.includeLimits,r=o?.clipOutliers??!1,l=o?.limits??"both";const s=()=>n({includeLimits:i,clipOutliers:r,limits:l});return{get:()=>({includeLimits:i,clipOutliers:r,limits:l}),sync(a,d,c=!1){e.innerHTML="",c&&e.appendChild(makeLimitsSelect(l,u=>{l=u,s()},t)),d&&e.appendChild(makeToggle("Axis includes limits",a,u=>{i=u,s()},t)),e.appendChild(makeToggle("Clip outliers",r,u=>{r=u,s()},t))},set(a){const d=a.limits??"both";return a.includeLimits===i&&a.clipOutliers===r&&d===l?!1:(i=a.includeLimits,r=a.clipOutliers,l=d,!0)}}}export function makeLinkedTestSelect(e,o,n,t={}){let i=o;const r=makeTestSelect(e,i,l=>{l!==i&&(i=l,n(l))},t);return{el:r,get:()=>i,set(l){return i===l||!e.some(s=>s.testNumber===l)?!1:(i=l,r.value=String(l),!0)}}}export const ALL_GROUPS="\0all";export function makeLinkedGroupSelect(e,o,n,t,i={}){let r=n;const l=c=>c??ALL_GROUPS,s=c=>c===ALL_GROUPS?null:c,a=makeLabeledSelect(o,[{value:ALL_GROUPS,label:"All groups"},...e.map(c=>({value:c,label:c}))],l(r),c=>{const u=s(c);u!==r&&(r=u,t(u))},{...i,hook:"group-scope"}),d=a.querySelector('[data-wmap-select="group-scope"]');return{el:a,get:()=>r,set(c){return r===c||c!==null&&!e.includes(c)?!1:(r=c,d&&(d.value=l(c)),!0)}}}export function makeWaferSelect(e,o,n,t={}){const{maxWidth:i="160px",allLabel:r="All wafers",ownerDocument:l=document}=t,s="\0all";return makeListSelect([{value:s,label:r},...e.map((a,d)=>({value:String(d),label:a.label??`#${d}`}))],o===null?s:String(o),a=>n(a===s?null:Number(a)),{maxWidth:i,ownerDocument:l,ariaLabel:r,searchPlaceholder:"Filter wafers\u2026"})}export function makeToggle(e,o,n,t=document){const i=t.createElement("label");Object.assign(i.style,{display:"inline-flex",alignItems:"center",gap:m.xs,fontSize:S.body,color:f.label,cursor:"pointer",userSelect:"none"}),L(i,"bare");const r=t.createElement("input");return r.type="checkbox",r.checked=o,r.style.cssText="margin:0;cursor:pointer;",r.addEventListener("change",()=>n(r.checked)),i.append(r,t.createTextNode(e)),i}export function makeLabeledSelect(e,o,n,t,i={}){const{maxWidth:r="160px",hook:l,ownerDocument:s=document}=i,a=s.createElement("label");a.textContent=e,Object.assign(a.style,{color:f.label,fontSize:S.body,display:"flex",alignItems:"center",gap:m.xs});const d=makeListSelect(o,n,t,{maxWidth:r,ownerDocument:s,hook:l,ariaLabel:e.replace(/:\s*$/,"")});return a.appendChild(d),a}export function makeChartGridWrap(e=document){const o=e.createElement("div");return o.dataset.wmapChartGrid="1",Object.assign(o.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:m.lg,flex:"0 0 auto"}),o}export function makeTooltip(e){const o=e.ownerDocument.createElement("div");return Object.assign(o.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:"rgba(30, 32, 40, 0.93)",color:"#f0f0f2",border:"1px solid rgba(255,255,255,0.10)",borderRadius:C.control,padding:`${m.sm} ${m.xl}`,fontSize:S.sub,fontFamily:S.family,maxWidth:"320px",whiteSpace:"normal",overflowWrap:"break-word",boxShadow:D.menu}),e.appendChild(o),o}export function attachChartTip(e,o,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",i=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,o,i.clientX,i.clientY)}),e.addEventListener("mousemove",i=>{n.style.display==="block"&&positionChartTooltip(n,o,i.clientX,i.clientY)}),e.addEventListener("mouseleave",()=>{n.style.display="none"}),e.addEventListener("click",()=>{n.style.display="none"}),e.addEventListener("focus",()=>{n.textContent=t,n.style.display="block";const i=e.getBoundingClientRect();positionChartTooltip(n,o,i.left+i.width/2,i.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,o,n,t){const i=o.getBoundingClientRect(),r=14,l=4;e.style.left="0",e.style.top="0";const s=e.offsetWidth,a=e.offsetHeight;let d=n-i.left+r,c=t-i.top+r;d+s+l>i.width&&(d=n-i.left-r-s),c+a+l>i.height&&(c=t-i.top-r-a),d=Math.max(l,d),c=Math.max(l,c),e.style.left=`${d}px`,e.style.top=`${c}px`}export const LIMIT_LABEL={test:{lo:"Lo limit",hi:"Hi limit"},spec:{lo:"LSL",hi:"USL"}},LIMIT_DASH={test:[3,3],spec:[10,4]},LIMITS_SHOWN_OPTIONS=[{value:"both",label:"Test + spec"},{value:"test",label:"Test limits"},{value:"spec",label:"Spec limits (LSL/USL)"},{value:"none",label:"None"}];function A(e,o){const[n,t]=o==="test"?[e?.limitLow,e?.limitHigh]:[e?.specLow,e?.specHigh],i=[];return n!==void 0&&Number.isFinite(n)&&i.push({value:n,kind:o,end:"lo",label:LIMIT_LABEL[o].lo}),t!==void 0&&Number.isFinite(t)&&i.push({value:t,kind:o,end:"hi",label:LIMIT_LABEL[o].hi}),i}export function hasBothLimitKinds(e){return A(e,"test").length>0&&A(e,"spec").length>0}export function limitLines(e,o="both"){if(o==="none")return[];const n=A(e,"test"),t=A(e,"spec"),i=o==="test"?n:o==="spec"?t:[...n,...t];return i.length?i:[...n,...t]}export function limitExtent(e){if(!e.length)return{};const o=e.map(n=>n.value);return{lo:ee(o),hi:te(o)}}export function stackLabelRows(e,o=4){const n=e.map((r,l)=>({...r,i:l})).sort((r,l)=>r.start-l.start),t=[],i=new Array(e.length).fill(0);for(const r of n){let l=t.findIndex(s=>r.start>=s+o);l===-1?(l=t.length,t.push(r.end)):t[l]=r.end,i[r.i]=l}return i}export function fillTextOnHalo(e,o,n,t,i){if(i){const r=e.measureText(o),l=2,s=e.fillStyle;e.fillStyle=i,e.fillRect(n-r.actualBoundingBoxLeft-l,t-r.actualBoundingBoxAscent-l,r.actualBoundingBoxLeft+r.actualBoundingBoxRight+2*l,r.actualBoundingBoxAscent+r.actualBoundingBoxDescent+2*l),e.fillStyle=s}e.fillText(o,n,t)}export function strokeLimitLine(e,o,n,t,i,r,l){e.save(),e.strokeStyle=n,e.lineWidth=1,e.setLineDash(LIMIT_DASH[o.kind]),e.beginPath(),e.moveTo(t,i),e.lineTo(r,l),e.stroke(),e.restore()}export function makeLimitsSelect(e,o,n=document){return makeLabeledSelect("Limits:",LIMITS_SHOWN_OPTIONS,e,t=>o(t),{maxWidth:"170px",hook:"limits-shown",ownerDocument:n})}export function robustFence(e,o=1.5){const n=Float64Array.from(e.filter(s=>Number.isFinite(s)));if(n.sort(),n.length<8)return null;const t=s=>{const a=(n.length-1)*s,d=Math.floor(a),c=Math.ceil(a);return d===c?n[d]:n[d]+(n[c]-n[d])*(a-d)},i=t(.25),r=t(.75),l=r-i;return l===0?null:{lo:i-o*l,hi:r+o*l}}export function shouldIncludeLimitsByDefault(e,o,n,t,i=1/3){if(n===void 0&&t===void 0||!Number.isFinite(e)||!Number.isFinite(o))return!1;const r=Math.min(e,n??e),s=Math.max(o,t??o)-r;if(s<=0)return!0;const a=o-e;return a===0?!0:a/s>=i}export function resolveAxisRange(e){const{dataMin:o,dataMax:n,limits:t=[],includeLimits:i,clipOutliers:r,values:l}=e;let s=o,a=n,d=0;if(r&&l?.length){const u=robustFence(l);if(u){const p=Math.max(s,u.lo),w=Math.min(a,u.hi);w>p&&(d=l.filter(v=>Number.isFinite(v)&&(v<p||v>w)).length,s=p,a=w)}}if(i)for(const u of t)s=Math.min(s,u.value),a=Math.max(a,u.value);s===a&&(s-=1,a+=1);const c=[];for(const u of t)u.value<s?c.push({value:u.value,label:u.label,side:"lo"}):u.value>a&&c.push({value:u.value,label:u.label,side:"hi"});return{lo:s,hi:a,offAxis:c,clippedCount:d}}export function limitLabelSide(e,o,n,t,i,r=3){const l=i?-1:1;return(l<0?e-r-o>=n:e+r+o<=t)?l:-l}export function drawOffAxisLimits(e,o,n,t,i,r,l){if(!o.length)return;e.save(),e.fillStyle=i,e.font=`${k(-1)}px system-ui, sans-serif`;const s={lo:0,hi:0},a=k(-1)+2;for(const{value:d,label:c,side:u}of o){const p=s[u]++*a,w=t==="horizontal"?u==="lo"?"\u2190":"\u2192":u==="lo"?"\u2193":"\u2191",v=u==="lo"?`${w} ${c} ${r(d)}`:`${c} ${r(d)} ${w}`;t==="horizontal"?(e.textBaseline="top",e.textAlign=u==="lo"?"left":"right",fillTextOnHalo(e,v,u==="lo"?n.left+2:n.right-2,n.top+2+p,l)):(e.textBaseline=u==="lo"?"bottom":"top",e.textAlign="left",fillTextOnHalo(e,v,n.left+4,u==="lo"?n.bottom-2-p:n.top+2+p,l))}e.restore()}
@@ -1 +1 @@
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
+ import{buildTestHistogramData as qt,collectTestValues as Ht,buildTestHistogramSeries as Jt,testValueExtent as At}from"../../stats/histogram.js";import{SPACE as lt,fontPx as R,FONT as Ct,CLR as $t}from"../toolbar.js";import{fmt as gt}from"../../renderer/fmt.js";import{niceStep as Zt,fitTicks as Et}from"../../renderer/axisTicks.js";import{QUANTITY as te,categorical as ee}from"./palette.js";import{cardShell as ne,observeResize as Wt,makeTooltip as ie,attachChartTip as oe,positionChartTooltip as Bt,makeLinkedTestSelect as se,makeWaferSelect as le,makeLinkedAxisPrefs as ae,renderEmptyState as yt,chartFillHeight as Dt,applyCanvasFlow as It,makeAxisFormat as Tt,horizontalTickSpacing as Pt,PADDING as mt,robustFence as Kt,shouldIncludeLimitsByDefault as Rt,drawOffAxisLimits as re,resolveAxisRange as ce,limitLines as Ot,limitExtent as de,hasBothLimitKinds as ue,stackLabelRows as me,strokeLimitLine as fe,chartSwatchCss as he,makeSeriesLegendItem as pe,prepareCanvas as Ft}from"./chartShell.js";import{escHtml as Mt,maxOf as ge}from"../../core/utils.js";const vt=230,Nt=36,xe=18;function zt(n){const C=new Set(n.map($=>$.kind)).size;return xe+Math.max(0,C-1)*(R(-1)+2)}function Gt(n,C,$,G,et,E,s){n.font=`${R(-1)}px system-ui, sans-serif`;const h=C.filter(i=>!$.has(i.value)).map(i=>{const b=G(i.value),W=n.measureText(i.label).width;return{l:i,x:b,start:b-W/2,end:b+W/2}}),U=me(h),nt=R(-1)+2;h.forEach(({l:i,x:b},W)=>{fe(n,i,s,b,et,b,E),n.fillStyle=s,n.textAlign="center",n.textBaseline="bottom",n.fillText(i.label,b,et-2-U[W]*nt)})}function Vt(n,C,$,G,et,E,s,h=4){const U=Math.max(1,Zt(E/h)),nt=G-$;n.save(),n.font=`${R(-1)}px system-ui, sans-serif`,n.textAlign="right",n.textBaseline="middle";for(let i=0;i<=E+1e-9;i+=U){const b=G-i/(E||1)*nt;n.strokeStyle=s.grid,n.lineWidth=1,n.beginPath(),n.moveTo(C,b),n.lineTo(C+et,b),n.stroke(),n.fillStyle=s.text,n.fillText(String(Math.round(i)),C-4,b)}n.strokeStyle=s.axis,n.lineWidth=1,n.beginPath(),n.moveTo(C,$),n.lineTo(C,G),n.stroke(),n.restore()}export function renderHistogramPanel(n){const{title:C="Test value distribution",items:$,testDefs:G,onSaveImage:et,groups:E}=n,{card:s,body:h,controlsRow:U}=ne(C,et,n.ownerDocument),nt=G.filter(a=>a.testNumber!==void 0);let i=n.selectedTestNumber??nt[0]?.testNumber??null,b=null,W=0;const kt=se(nt,i,a=>{i=a,q(),n.onTestChange?.(a)},{maxWidth:"200px",ownerDocument:s.ownerDocument}),jt=kt.el;U.appendChild(jt);const ft=$.length>1?le($,b,a=>{b=a,q()},{ownerDocument:s.ownerDocument}):null;ft&&U.appendChild(ft);function _t(a){return y===a||a!==null&&!(E??[]).some(T=>T.key===a)?!1:(y=a,q(),!0)}const xt=s.ownerDocument.createElement("span");Object.assign(xt.style,{display:"inline-flex",gap:lt.lg,alignItems:"center"}),U.appendChild(xt);const at=ae(xt,n.axisPrefs,a=>{n.onAxisPrefsChange?.(a),q()},s.ownerDocument),B=ie(s);let rt=null,y=null;function it(a){const T=G.find(D=>D.testNumber===a),V=Ot(T,at.get().limits),{lo:w,hi:M}=de(V);return{unit:T?.unit,lines:V,limitLow:w,limitHigh:M,hasLimits:Ot(T).length>0,hasBothKinds:ue(T)}}function q(){const{includeLimits:a,clipOutliers:T}=at.get();if(rt?.disconnect(),rt=null,h.innerHTML="",i===null){yt(h,"No parametric test data available for a histogram.");return}const V=!!E&&E.length>0;let w=!1,M;if(V){const{limitLow:d,limitHigh:g}=it(i),x=E.flatMap(l=>l.items),{min:e,max:f}=At(x,i);if(w=a??Rt(e,f,d,g),T){const l=Ht(x,i),u=Kt(l);u&&(M={lo:Math.max(e,u.lo),hi:Math.min(f,u.hi)},W=l.reduce((Z,K)=>Z+(K<M.lo||K>M.hi?1:0),0))}M||(W=0)}const D=V?Jt(E,i,16,w?it(i).limitLow:void 0,w?it(i).limitHigh:void 0,M):null;if(ft&&(ft.style.display=D?"none":""),D){const{hasLimits:d,hasBothKinds:g}=it(i);if(at.sync(w,d,g),D.series.length===0){yt(h,"No parametric test data available for a histogram.");return}Xt(D);return}const j=b!==null?[$[b]]:$,{unit:I,lines:_,limitLow:S,limitHigh:X,hasLimits:ct,hasBothKinds:ht}=it(i),dt=Ht(j,i),{min:ut,max:pt}=At(j,i),P=a??Rt(ut,pt,S,X);at.sync(P,ct,ht);const ot=T?Kt(dt):null,O=ot?{lo:Math.max(ut,ot.lo),hi:Math.min(pt,ot.hi)}:void 0;W=O?dt.filter(d=>d<O.lo||d>O.hi).length:0;const H=qt(j,i,16,P?S:void 0,P?X:void 0,O);if(H.length===0){yt(h,"No parametric test data available for a histogram.");return}const o=Math.max(ge(H.map(d=>d.count)),1),p=s.ownerDocument.createElement("div");Object.assign(p.style,{fontSize:Ct.body,color:$t.label,marginBottom:lt.xxs}),p.textContent=`max ${o} dies/bucket`+(W?` \xB7 ${W} value${W===1?"":"s"} outside clipped range`:""),h.appendChild(p);const r=s.ownerDocument.createElement("canvas");r.style.display="block",r.style.cursor="default",h.appendChild(r);let t=-1;const c=H[0].rangeLow,m=H[H.length-1].rangeHigh,v=m-c||1,F=Math.max(Math.abs(c),Math.abs(m));function J(d){const g=mt+36,x=r.clientWidth-g-mt,e=zt(_),f=d-Nt-e;return{plotX:g,plotMaxWidth:Math.max(10,x),plotMaxHeight:f,plotTop:e}}function k(d){const{plotX:g,plotMaxWidth:x}=J(vt);if(d<g||d>g+x)return-1;const e=Math.floor((d-g)/x*H.length);return e>=0&&e<H.length?e:-1}function A(){It(r,p);const d=h.clientWidth,g=Dt(s,h,r,vt),x=Ft(r,s,d,g);if(!x)return;const{ctx:e,theme:f}=x,{plotX:l,plotMaxWidth:u,plotMaxHeight:Z,plotTop:K}=J(g),L=K+Z,tt=u/H.length;Vt(e,l,K,L,u,o,{text:f.textMuted,axis:f.border,grid:f.grid}),H.forEach((z,Q)=>{const wt=z.count/o*Z,Lt=l+Q*tt,St=L-wt;e.fillStyle=Q===t?f.text:te,e.globalAlpha=Q===t?1:.7,e.fillRect(Lt+1,St,Math.max(1,tt-2),wt),e.globalAlpha=1,Q===t&&(e.fillStyle=f.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${z.count}`,Lt+tt/2,St-2))});const st=z=>l+(z-c)/v*u;e.font=`${R(-1)}px system-ui, sans-serif`;const{offAxis:N}=ce({dataMin:c,dataMax:m,limits:_,includeLimits:!1});re(e,N,{left:l,right:l+u,top:K,bottom:L},"horizontal",f.limitLine,Tt(F,I).tick),Gt(e,_,new Set(N.map(z=>z.value)),st,K,L,f.limitLine),e.font=`${R(-1)}px system-ui, sans-serif`,e.strokeStyle=f.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,L),e.lineTo(l+u,L),e.stroke(),e.fillStyle=f.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:Y,step:Ut}=Et(c,m,u,Pt(e,F,I)),bt=Tt(F,I,Ut||void 0);for(const z of Y){const Q=l+(z-c)/v*u;e.beginPath(),e.moveTo(Q,L),e.lineTo(Q,L+4),e.stroke(),e.fillText(bt.tick(z),Q,L+6)}bt.unitLabel&&(e.save(),e.font=`${R(-1)}px system-ui, sans-serif`,e.fillStyle=f.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${bt.unitLabel})`,l+u,L+20),e.restore())}r.addEventListener("mousemove",d=>{const g=r.getBoundingClientRect(),x=k(d.clientX-g.left);if(x!==t&&(t=x,A()),x>=0){const e=H[x];B.innerHTML=`<strong>${Mt(`${gt(e.rangeLow,I,"engineering")} \u2013 ${gt(e.rangeHigh,I,"engineering")}`)}</strong><br>${e.count} dies`,B.style.display="block",Bt(B,s,d.clientX,d.clientY)}else B.style.display="none"}),r.addEventListener("mouseleave",()=>{t!==-1&&(t=-1,A()),B.style.display="none"}),rt=Wt(s,()=>A()),A()}function Xt(a){const{unit:T,lines:V}=it(i),w=a.ranges,M=a.series,D=ee;y&&!M.some(o=>o.groupKey===y)&&(y=null);const j=Math.max(1,...M.flatMap(o=>o.counts)),I=s.ownerDocument.createElement("div");Object.assign(I.style,{fontSize:Ct.body,color:$t.label,marginBottom:lt.xxs}),I.textContent=`${M.length} groups \xB7 max ${j} dies/bucket`,h.appendChild(I);const _=s.ownerDocument.createElement("div");Object.assign(_.style,{display:"flex",flexWrap:"wrap",gap:lt.sm,marginTop:lt.md,marginBottom:lt.xl}),M.forEach((o,p)=>{const r=pe(o.groupKey,D(p),s.ownerDocument),t=r.el;oe(t,s,B,`${o.groupKey} \u2014 click to emphasize (dim the rest)`),r.setState({selected:y===o.groupKey,dimmed:y!==null&&y!==o.groupKey}),t.addEventListener("click",()=>{const c=y===o.groupKey?null:o.groupKey;_t(c),n.onGroupChange?.(c)}),_.appendChild(t)}),h.appendChild(_);const S=s.ownerDocument.createElement("canvas");S.style.display="block",S.style.cursor="default",h.appendChild(S);const X=w[0].rangeLow,ct=w[w.length-1].rangeHigh,ht=ct-X||1,dt=Math.max(Math.abs(X),Math.abs(ct)),ut=o=>{const p=(o.ownerDocument.defaultView??window).getComputedStyle(o);return o.offsetHeight+(parseFloat(p.marginTop)||0)+(parseFloat(p.marginBottom)||0)},pt=()=>ut(I)+ut(_);let P=-1,ot={plotX:mt+36,plotMaxWidth:10,barWidth:10};function O(){It(S,pt());const o=h.clientWidth,p=Dt(s,h,S,vt),r=Ft(S,s,o,p);if(!r)return;const{ctx:t,theme:c}=r,m=mt+36,v=Math.max(10,o-m-mt),F=zt(V),J=p-Nt-F,k=F+J,A=v/w.length;ot={plotX:m,plotMaxWidth:v,barWidth:A},Vt(t,m,F,k,v,j,{text:c.textMuted,axis:c.border,grid:c.grid}),P>=0&&(t.fillStyle=c.bgHover,t.globalAlpha=.5,t.fillRect(m+P*A,F,A,J),t.globalAlpha=1);const d=M.map((l,u)=>({s:l,i:u})).sort((l,u)=>(l.s.groupKey===y?1:0)-(u.s.groupKey===y?1:0));for(const{s:l,i:u}of d){const Z=y===l.groupKey,K=y!==null&&!Z,L=D(u);t.strokeStyle=L,t.fillStyle=L,t.lineWidth=Z?2:1,t.globalAlpha=K?.12:y?.32:.22,t.beginPath(),t.moveTo(m,k),l.counts.forEach((tt,st)=>{const N=m+st*A,Y=k-tt/j*J;t.lineTo(N,Y),t.lineTo(N+A,Y)}),t.lineTo(m+v,k),t.closePath(),t.fill(),t.globalAlpha=K?.3:1,t.beginPath(),l.counts.forEach((tt,st)=>{const N=m+st*A,Y=k-tt/j*J;st===0?t.moveTo(N,Y):t.lineTo(N,Y),t.lineTo(N+A,Y)}),t.stroke()}t.globalAlpha=1;const g=l=>m+(l-X)/ht*v;t.font=`${R(-1)}px system-ui, sans-serif`,Gt(t,V,new Set,g,F,k,c.limitLine),t.font=`${R(-1)}px system-ui, sans-serif`,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.moveTo(m,k),t.lineTo(m+v,k),t.stroke(),t.fillStyle=c.textMuted,t.textAlign="center",t.textBaseline="top";const{ticks:x,step:e}=Et(X,ct,v,Pt(t,dt,T)),f=Tt(dt,T,e||void 0);for(const l of x){const u=m+(l-X)/ht*v;t.beginPath(),t.moveTo(u,k),t.lineTo(u,k+4),t.stroke(),t.fillText(f.tick(l),u,k+6)}f.unitLabel&&(t.save(),t.font=`${R(-1)}px system-ui, sans-serif`,t.fillStyle=c.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${f.unitLabel})`,m+v,k+20),t.restore())}function H(o){const{plotX:p,plotMaxWidth:r,barWidth:t}=ot;if(o<p||o>p+r)return-1;const c=Math.floor((o-p)/t);return c>=0&&c<w.length?c:-1}S.addEventListener("mousemove",o=>{const p=S.getBoundingClientRect(),r=H(o.clientX-p.left);if(r!==P&&(P=r,O()),r>=0){const t=w[r],c=M.map((m,v)=>`<span style="${he(D(v))};margin-right:4px"></span>${Mt(`${m.groupKey}: ${m.counts[r]}`)}`).join("<br>");B.innerHTML=`<strong>${Mt(`${gt(t.rangeLow,T,"engineering")} \u2013 ${gt(t.rangeHigh,T,"engineering")}`)}</strong><br>${c}`,B.style.display="block",Bt(B,s,o.clientX,o.clientY)}else B.style.display="none"}),S.addEventListener("mouseleave",()=>{P!==-1&&(P=-1,O()),B.style.display="none"}),rt=Wt(s,()=>O()),O()}q();function Yt(a){kt.set(a)&&(i=a,q())}function Qt(a){at.set(a)&&q()}return{card:s,setTest:Yt,setAxisPrefs:Qt,destroy:()=>rt?.disconnect()}}
@@ -1,10 +1,14 @@
1
- import { type ScatterItem } from '../../stats/scatter.js';
1
+ import { type ScatterItem, type ScatterPoint } from '../../stats/scatter.js';
2
2
  import { type FacetItem } from '../../stats/facets.js';
3
3
  import type { TestDef } from '../../renderer/buildWaferMap.js';
4
- import { type SaveImageHandler } from './chartShell.js';
4
+ import { type SaveImageHandler, type AxisPrefs } from './chartShell.js';
5
5
  type ScatterPanelItem = FacetItem & ScatterItem & {
6
6
  label?: string;
7
7
  };
8
+ export declare const NO_BIN_CATEGORY = "none";
9
+ export declare function binCategoryOf(hbin: number | undefined): string;
10
+ /** Legend order: bins ascending, then "No bin data" last. */
11
+ export declare function binCategories(points: readonly Pick<ScatterPoint, 'hbin'>[]): string[];
8
12
  export interface ScatterPanelOptions {
9
13
  title?: string;
10
14
  items: ScatterPanelItem[];
@@ -30,6 +34,14 @@ export interface ScatterPanelOptions {
30
34
  key: string;
31
35
  items: ScatterPanelItem[];
32
36
  }[];
37
+ /**
38
+ * The Insights tab's shared axis preferences. Scatter reads only `limits`
39
+ * (which limit lines to draw), and offers the "Limits:" choice when either
40
+ * axis's test has both test and spec limits.
41
+ */
42
+ axisPrefs?: AxisPrefs;
43
+ /** Called when the user changes the "Limits:" choice here. */
44
+ onAxisPrefsChange?: (prefs: AxisPrefs) => void;
33
45
  /** Document to build this panel's DOM into. Default `document` — pass the
34
46
  * host's own `ownerDocument` when the container might live in a
35
47
  * different document (e.g. a gallery card detached into its own popup
@@ -40,6 +52,8 @@ export interface ScatterPanelHandle {
40
52
  card: HTMLElement;
41
53
  /** Cross-panel link (e.g. from the correlation matrix): switch X/Y in place. */
42
54
  setXY: (xTestNumber: number, yTestNumber: number) => void;
55
+ /** Adopt axis preferences chosen in another panel (only `limits` applies). */
56
+ setAxisPrefs: (prefs: AxisPrefs) => void;
43
57
  destroy: () => void;
44
58
  }
45
59
  export declare function renderScatterPanel(options: ScatterPanelOptions): ScatterPanelHandle;
@@ -1 +1 @@
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()}}
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 +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
+ 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 +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 w}from"../toolbar.js";import{cardShell as A,makeTooltip as O,makeSegmented as j,renderEmptyState as y}from"./chartShell.js";import{renderGroupedBarPlot as M}from"./groupedBarPlot.js";import{escHtml as g}from"../../core/utils.js";const k=100,G=8,U={spec:"Spec limits",testFlag:"Tester flag",functional:"Functional"},K={spec:"Parametric pass rate \xB7 spec limits",testFlag:"Parametric pass rate \xB7 tester flag",functional:"Functional test pass rate"};export function renderTestPassRatePanel(f){const{groups:u,testDefs:b,onSaveImage:$}=f,p=f.ownerDocument??document,P=u.flatMap(e=>e.items.flatMap(a=>a.dies??[])),d=["spec","testFlag","functional"].filter(e=>B(b,e,P));let s=d[0]??"spec";const h=e=>K[e],{card:o,heading:v,body:i,controlsRow:x}=A(h(s),$,p);o.style.alignSelf="start",d.length>1&&x.appendChild(j(d.map(e=>[e,U[e]]),s,e=>{s=e,v.textContent=h(s),L()},p));const n=p.createElement("div");Object.assign(n.style,{color:w.label,fontSize:_.body,marginBottom:H.sm}),o.insertBefore(n,i);const D=O(o);let m=null;function L(){if(m?.destroy(),m=null,i.innerHTML="",d.length===0){n.textContent="",y(i,"No tests with a pass/fail verdict \u2014 parametric tests need spec limits, functional tests need recorded results.");return}const e=F(u,b,s);if(e.rows.length===0){n.textContent="",y(i,s==="spec"?"No parametric test has spec limits to judge against.":s==="testFlag"?"No parametric test has a recorded tester verdict.":"No functional test has recorded results.");return}const a=e.groups.length>1,R=s==="spec"?"Dies within spec limits, per test":s==="testFlag"?"Dies the tester marked pass, per test":"Dies passing the recorded verdict, per test";if(n.textContent=R+(a?" \xB7 one bar per group":"")+" \xB7 worst first",e.disagreementDies){const t=p.createElement("span");t.textContent=` \xB7 ${e.disagreementDies.toLocaleString()} die${e.disagreementDies===1?"":"s"} judged differently by limits and tester flag`,Object.assign(t.style,{color:w.warnText}),n.appendChild(t)}const S=e.rows,C=t=>a?I(t):N;m=M(o,i,D,{rows:S.map(t=>({label:t.label,bars:t.byGroup.map(l=>({fraction:l.passRatePercent===null?null:l.passRatePercent/100})),trailing:t.overall.passRatePercent===null?"\u2014":`${t.overall.passRatePercent.toFixed(1)}%`})),groups:e.groups,labelWidth:k,maxVisibleRows:G,showLegend:a,maxLabelChars:14,emptyBarText:"no data",colorOf:C,tooltipHtml:(t,l)=>{const r=S[t],c=r.byGroup[l],T=a?` \xB7 ${g(String(e.groups[l]))}`:"";if(c.passRatePercent===null)return`<strong>${g(r.label)}</strong>${T}<br>no dies with a verdict`;const E=s==="spec"&&!a&&(r.failLowDies||r.failHighDies)?`<br>${r.failLowDies??0} below LSL \xB7 ${r.failHighDies??0} above USL`:"";return`<strong>${g(r.label)}</strong>${T}<br>${c.passRatePercent.toFixed(1)}% pass<br>${c.passDies.toLocaleString()} pass \xB7 ${c.failDies.toLocaleString()} fail \xB7 n = ${c.totalDies.toLocaleString()}`+E}})}return L(),{card:o,destroy:()=>{m?.destroy(),D.remove(),o.remove()}}}
1
+ 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 +1 @@
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()}}}
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()}}}
@@ -15,7 +15,7 @@ export interface DieListDisplayOptions {
15
15
  * Show a "View die list" link inside the Summary panel (`renderWaferMap`)
16
16
  * / lot Summary panel (`renderWaferGallery`) that opens this wafer's — or,
17
17
  * in the gallery, the whole lot's — dies as a table in a modal, with the
18
- * same "Export CSV" button `buildDieListSection` always has. **Default
18
+ * same "Export CSV" button every die list has. **Default
19
19
  * `true`** — set `enabled: false` to hide it. Requires a Summary panel to
20
20
  * be reachable at all (`RenderOptions.summaryPanel` / `GalleryOptions.summaryPanel`),
21
21
  * since that panel is this link's only home; irrelevant otherwise.
@@ -18,20 +18,9 @@
18
18
  * `suggestedName` argument of their save hooks.
19
19
  */
20
20
  import { type SaveImageHandler, type SaveTextHandler } from './toolbar.js';
21
- /**
22
- * Tell a host that passes `downloadFilename` its meaning changes in 0.31.0.
23
- * Until then it keeps its documented meaning — the whole name of the map or
24
- * gallery PNG — and nothing else reads it; changing that in a patch release
25
- * would break hosts that match the name their `onSaveImage` receives.
26
- */
27
- export declare function noticeDownloadFilenameChange(downloadFilename: string | undefined): void;
28
21
  /** What a renderer knows about the data behind an export. */
29
22
  export interface ExportContext {
30
- /**
31
- * Leads the name, and suppresses any context it already names. Reserved for
32
- * `downloadFilename` from 0.31.0; no renderer sets it before then (see
33
- * `noticeDownloadFilenameChange`).
34
- */
23
+ /** The host's `downloadFilename`: leads the name, and suppresses any context it already names. */
35
24
  prefix?: string;
36
25
  /** Distinct lot IDs covered, in display order. */
37
26
  lots: string[];
@@ -1 +1 @@
1
- import{downloadBlob as u}from"./toolbar.js";import{noticeOnce as _}from"../renderer/deprecate.js";export function noticeDownloadFilenameChange(e){e!=null&&_("downloadFilename",'downloadFilename becomes a prefix in 0.31.0: the library will append the lot, wafer and content (e.g. "<downloadFilename>_W05_hard-bin.png") and apply it to every saved file, CSVs included. Until then it names only the map or gallery PNG, as before.')}const $=60,m=180,g=/^(con|prn|aux|nul|com\d|lpt\d)$/i;function f(e){return e.normalize("NFC").replace(/[^\p{L}\p{N}._+-]+/gu,"-").replace(/-{2,}/g,"-").replace(/^[-._]+|[-._]+$/g,"").slice(0,$).replace(/[-._]+$/g,"")}function c(e,n){return/^\d+$/.test(n)?`${e}-${n}`:n}function w(e,n){const o=`_${e.toLowerCase().replace(/[-_.\s]+/g,"_")}_`,r=`_${n.toLowerCase().replace(/[-_.\s]+/g,"_")}_`;return o.includes(r)}export function buildExportFilename(e,n){const o=n.lastIndexOf("."),r=o>=0?n.slice(o):"",l=f(o>=0?n.slice(0,o):n).toLowerCase(),t=e.prefix?.trim()?f(e.prefix):"",a=[];if(e.lots.length===1?a.push({raw:e.lots[0],text:c("lot",e.lots[0])}):e.lots.length>1&&a.push({raw:`${e.lots.length}-lots`,text:`${e.lots.length}-lots`}),e.stackedWafers!==void 0){const i=e.stackedWafers===!0?"stacked":`stacked-${e.stackedWafers}-wafer${e.stackedWafers===1?"":"s"}`;a.push({raw:i,text:i})}else e.wafer!==void 0?a.push({raw:e.wafer,text:c("wafer",e.wafer)}):e.waferCount!==void 0&&e.waferCount>1&&a.push({raw:`${e.waferCount}-wafers`,text:`${e.waferCount}-wafers`});const p=[t];for(const{raw:i,text:h}of a){const d=f(h);d&&(t&&(w(t,f(i))||w(t,d))||p.push(d))}p.push(l);let s=p.filter(Boolean).join("_").slice(0,m).replace(/[-_.]+$/g,"");return s||(s="wafermap"),g.test(s)&&(s=`wafermap-${s}`),`${s}${r}`}export function withExportContext(e,n,o){return{onSaveImage:(r,l)=>{const t=buildExportFilename(e(),l);if(n)return n(r,t);u(r,t)},onSaveText:(r,l,t)=>{const a=buildExportFilename(e(),l);if(o)return o(r,a,t);u(new Blob([r],{type:t}),a)}}}
1
+ import{downloadBlob as w}from"./toolbar.js";const _=60,h=180,g=/^(con|prn|aux|nul|com\d|lpt\d)$/i;function i(e){return e.normalize("NFC").replace(/[^\p{L}\p{N}._+-]+/gu,"-").replace(/-{2,}/g,"-").replace(/^[-._]+|[-._]+$/g,"").slice(0,_).replace(/[-._]+$/g,"")}function c(e,t){return/^\d+$/.test(t)?`${e}-${t}`:t}function d(e,t){const r=`_${e.toLowerCase().replace(/[-_.\s]+/g,"_")}_`,n=`_${t.toLowerCase().replace(/[-_.\s]+/g,"_")}_`;return r.includes(n)}export function buildExportFilename(e,t){const r=t.lastIndexOf("."),n=r>=0?t.slice(r):"",f=i(r>=0?t.slice(0,r):t).toLowerCase(),s=e.prefix?.trim()?i(e.prefix):"",o=[];if(e.lots.length===1?o.push({raw:e.lots[0],text:c("lot",e.lots[0])}):e.lots.length>1&&o.push({raw:`${e.lots.length}-lots`,text:`${e.lots.length}-lots`}),e.stackedWafers!==void 0){const l=e.stackedWafers===!0?"stacked":`stacked-${e.stackedWafers}-wafer${e.stackedWafers===1?"":"s"}`;o.push({raw:l,text:l})}else e.wafer!==void 0?o.push({raw:e.wafer,text:c("wafer",e.wafer)}):e.waferCount!==void 0&&e.waferCount>1&&o.push({raw:`${e.waferCount}-wafers`,text:`${e.waferCount}-wafers`});const u=[s];for(const{raw:l,text:$}of o){const p=i($);p&&(s&&(d(s,i(l))||d(s,p))||u.push(p))}u.push(f);let a=u.filter(Boolean).join("_").slice(0,h).replace(/[-_.]+$/g,"");return a||(a="wafermap"),g.test(a)&&(a=`wafermap-${a}`),`${a}${n}`}export function withExportContext(e,t,r){return{onSaveImage:(n,f)=>{const s=buildExportFilename(e(),f);if(t)return t(n,s);w(n,s)},onSaveText:(n,f,s)=>{const o=buildExportFilename(e(),f);if(r)return r(n,o,s);w(new Blob([n],{type:s}),o)}}}
@@ -1,11 +1,11 @@
1
1
  export { WMAP_VERSION, WMAP_BUILD_TIME } from './version.js';
2
- export type { ToCanvasOptions, ToCanvasResult, HitTarget, ViewportTransform } from './toCanvas.js';
2
+ export type { ToCanvasOptions, ViewportTransform } from './toCanvas.js';
3
3
  export { renderWaferMap } from './renderWaferMap.js';
4
4
  export type { RenderOptions, WaferMapController, WaferViewOptions, WaferPreferences, WaferDisplayState } from './renderWaferMap.js';
5
5
  export { renderWaferGallery } from './renderWaferGallery.js';
6
6
  export type { WaferMapDisplayItem, WaferMapDisplayItemFactory, GalleryOptions, GalleryController } from './renderWaferGallery.js';
7
7
  export type { SummaryPanelOptions, FindingsNotice } from './summaryPanel.js';
8
- export type { DieListOptions, DieListDisplayOptions } from './dieList.js';
8
+ export type { DieListDisplayOptions } from './dieList.js';
9
9
  export { collectWarnings, severityOf } from './warnings.js';
10
10
  export type { WarningsOptions } from './warnings.js';
11
11
  export type { InsightsOptions, InsightsView } from './insightsTab.js';
@@ -16,5 +16,4 @@ export { openWaferMapGuide } from './toolbar.js';
16
16
  export { ICONS } from './icons.js';
17
17
  export type { OverlayHandle } from './toolbar.js';
18
18
  export { openReportModal } from './toolbar.js';
19
- export { toCanvas, buildDieListSection, } from './deprecated.js';
20
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- export{WMAP_VERSION,WMAP_BUILD_TIME}from"./version.js";export{renderWaferMap}from"./renderWaferMap.js";export{renderWaferGallery}from"./renderWaferGallery.js";export{collectWarnings,severityOf}from"./warnings.js";export{setDetachWindowOpener}from"./toolbar.js";export{openWaferMapGuide}from"./toolbar.js";export{ICONS}from"./icons.js";export{openReportModal}from"./toolbar.js";export{toCanvas,buildDieListSection}from"./deprecated.js";
1
+ export{WMAP_VERSION,WMAP_BUILD_TIME}from"./version.js";export{renderWaferMap}from"./renderWaferMap.js";export{renderWaferGallery}from"./renderWaferGallery.js";export{collectWarnings,severityOf}from"./warnings.js";export{setDetachWindowOpener}from"./toolbar.js";export{openWaferMapGuide}from"./toolbar.js";export{ICONS}from"./icons.js";export{openReportModal}from"./toolbar.js";
@@ -2,7 +2,7 @@ import type { LotStatsSummary } from '../stats/types.js';
2
2
  import type { WaferMapDisplayItem } from './renderWaferGallery.js';
3
3
  import type { BinColors } from '../renderer/binColors.js';
4
4
  import { type SaveImageHandler, type SaveTextHandler } from './toolbar.js';
5
- import type { SweepSpec } from '../stats/sweep.js';
5
+ import { type SweepSpec } from '../stats/sweep.js';
6
6
  export type InsightsView = 'overview' | 'distributions' | 'correlation' | 'sweeps';
7
7
  /** Public option shape for `RenderOptions.insights`/`GalleryOptions.insights`. */
8
8
  export interface InsightsOptions {
@@ -65,6 +65,14 @@ export interface InsightsOptions {
65
65
  * the syntax is one syntax library-wide.
66
66
  */
67
67
  sweeps?: SweepSpec[];
68
+ /**
69
+ * Offers a "Remove" button on the Sweeps tab's notice about sweeps that name
70
+ * none of the data's tests — typically sweeps a host kept from another test
71
+ * program. Called with those sweeps' ids; the host drops them and re-renders
72
+ * with the remaining `sweeps`. Without it the notice still shows, with no
73
+ * button. The library never edits the host's sweep list itself.
74
+ */
75
+ onRemoveSweeps?: (ids: string[]) => void;
68
76
  /**
69
77
  * Open the Insights view on mount instead of starting on the map/grid.
70
78
  * Default false — the tab is offered, the map is what you see first.
@@ -123,6 +131,8 @@ export interface InsightsTabDeps {
123
131
  defaultView?: InsightsView;
124
132
  /** Sweep definitions to render in the Distributions view — see `InsightsOptions.sweeps`. */
125
133
  sweeps?: SweepSpec[];
134
+ /** See `InsightsOptions.onRemoveSweeps`. */
135
+ onRemoveSweeps?: (ids: string[]) => void;
126
136
  /**
127
137
  * When provided, the tab bar gets a leading "‹ Map"/"‹ Gallery" tab that
128
138
  * exits Insights back to the host's normal view — one visible navigation