@wafertools/wafermap 0.24.3 → 0.25.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.
package/CHANGELOG.md CHANGED
@@ -22,6 +22,40 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
25
+ ## [0.25.0] — 2026-08-25
26
+
27
+ ### Added
28
+
29
+ - `WaferViewOptions.showLegend` (`renderWaferMap`) — hide the legend/colorbar entirely, for a host that provides its own equivalent control. Default `true`, matching prior behaviour.
30
+ - `WaferViewOptions.markFailingDies` / `ToCanvasOptions.markFailingDies` — draws a diagonal hatch on dies whose bin is not a pass bin, giving pass/fail a second channel that isn't hue. Bin modes only; opt in from the Colour scheme menu. Default `false`.
31
+ - `GalleryOptions.perCardLegend` — lets a gallery card grow its own legend again (its distinct value: highlighting a bin on one card independently of the rest, e.g. after changing that card's plot mode). Default `false`; the toolbar shows the toggle disabled with a stated reason where it genuinely cannot apply — a data-ranged value-mode gallery, where each card is normalised to its own min/max and no shared bar could describe them.
32
+ - `ViewOptions.passBins` (`buildView`) / `ViewRect.binFail` — which bins count as passing, consulted only to set the new failing-die marker above, never to colour anything (bin colour always comes from the colour scheme). Default `[1]`. Any bin can be a pass bin or a fail bin, bin 1 included — this must never be derived from the bin number.
33
+
34
+ ### Fixed
35
+
36
+ - **The gallery's sticky toolbar/legend could scroll out of view**, in two different ways depending on the host's own scroll setup: a page that scrolls at the document level had a stray `overflow-y: auto` on `html`/`body`, and a page with its own bounded scroll container had `overflow-y: auto` (or `overflow: hidden`) on a box that was never actually height-bound — both silently give `position: sticky` a *non-scrolling* containing block, indistinguishable from "not sticking" at all. Fixed across every one of the library's own gallery demo pages (`docs/examples/real-data.html`, `mixedwm38.html`, `statistics.html`, `theming.html`, `showcase.html`) as worked examples of the fix, and documented as its own `UI_STANDARDS.md` incident so the same mistake is recognisable in host code, not just here.
37
+ - **Two sequential z-index regressions in the gallery**: per-card toolbars floating above the gallery's own sticky toolbar when scrolled, then — after a first fix — the sticky legend obscuring its own dropdown menus. Root cause was a bare `zIndex` literal being compared across an accidentally-shared stacking context (`position: relative` without its own `z-index` does not create one, however much it looks like it should). Fixed generically rather than tier-bumped again: the card grid now contains its own stacking context (`isolation: isolate`), and every toolbar/chart dropdown and cascading submenu — map, gallery, and chart cards alike — now renders into one shared, always-elevated layer (`menuLayerFor`) instead of each call site reasoning about z-index relative to whatever chrome happens to be nearby.
38
+ - **A value-mode gallery with spec-ranged limits silently lost its colorbar entirely.** The per-card-legend gating checked `colorbarRangeMode === 'data'` (true when each card is scaled to its own range) but a *spec*-ranged value gallery has an identical range on every card and has no such problem — it was being suppressed anyway, gallery-wide, with nothing standing in for it.
39
+ - **The gallery's "Wafers" tab name implied it listed every wafer**, but it lists only wafers with their own findings — a 13-wafer lot could show 8 rows, and the two lowest-yielding wafers, having no findings, were among those silently missing. A user scanning for problem wafers read the absence as "these are fine." Renamed to "Findings".
40
+ - **The gallery's Lot-tab sidebar duplicated the metadata table already shown in the gallery's own top strip**, verified byte-identical against a live 13-wafer lot — a second copy that cost a third of the sidebar's width for zero new information.
41
+ - **Map and legend titles rendered near-black on every dark theme**, reading as a smudge rather than text — `drawTitleFitted`'s default text colour was a hardcoded `#333` rather than the resolved theme colour; the one call site that looked right was the only one passing a colour explicitly.
42
+ - **The floating legend covered the entire wafer on small gallery cards** (as small as 86×50px) instead of acting as a legend, and its title floated unprotected on top of the map, half-hidden behind dies, instead of inside the legend's own plate. The legend is now suppressed below a 150×120px canvas floor, and its title moved into the plate's own reserved heading row.
43
+ - **The correlation chart's r-value could never actually display**, on any lot: its cell width (`PREF_CELL`, 26px) sat structurally below the minimum width the label needed to render (`R_LABEL_MIN_CELL`, 28px) — dead code that had never once fired. Raised to 44px. Its label contrast also moved from a fixed `|r| > 0.6` heuristic to a real luminance-based contrast function.
44
+ - **The capability chart's axis labels assumed every test had spec limits** (hardcoded `USL`/`LSL`), mislabelling any test analysed with no limits defined. Labels now derive from the actual composition of spec'd/unspec'd tests in the current view — `USL`/`LSL` where any are spec'd, `max`/`min` where none are, and a `normalised (per test)` axis title for a mixed view.
45
+ - **The gallery picked the smallest workable column width even when a comfortable one would fit** — a 1171px canvas card, for example, rendered at 285px instead of 434px. Column sizing now tries the largest comfortable width that still fits at least one column, falling back to the hard floor only when even that doesn't fit.
46
+ - A ResizeObserver feedback loop in the gallery grid and in chart cards, from redundant grid-template/`minHeight` writes on every resize tick even when the value hadn't changed; both now coalesce onto `requestAnimationFrame` and skip the write when nothing changed.
47
+ - The detached-card placeholder always read "Opened in its own window", even when the card had actually opened in the in-app wafer viewer instead of a real popup window.
48
+
49
+ ### Changed
50
+
51
+ - Themed tooltips (`attachChartTip`) replace native `title` attributes on the histogram and scatter charts' legend swatches, matching the rest of the toolbar's tooltip styling instead of falling back to the browser's own delayed, unstyled tooltip.
52
+
53
+ ### Internal
54
+
55
+ - `UI_STANDARDS.md` gained a new documented incident ("position: sticky or fixed") covering the sticky/z-index bugs above, and the general lesson: a bare z-index literal only means what it looks like if something between it and whatever it's compared against already established a real stacking context.
56
+ - New Check 3 in `scripts/check-overlay-conventions.mjs` (wired into `npm run check`): flags a `position: sticky`/`fixed` element with a bare-literal `zIndex` and no nearby comment naming what stacking context contains it — the question has to be asked in writing, not skipped under time pressure.
57
+ - **`scripts/check-changelog.mjs` blocked its own release process.** `npm version`'s `preversion` hook (`npm run verify` → `npm run check` → this script) runs *before* npm bumps `package.json` — so on a correctly-prepared release, where the CHANGELOG entry for the upcoming version is deliberately written first, the check saw `package.json` one release behind the heading it was about to release and aborted `npm version` before it did anything. Hit twice in one day. Fixed by trusting `npm_new_version` (set once by the outermost `npm version` and confirmed to survive the `preversion → verify → check` nesting unchanged, unlike `npm_lifecycle_event`, which resets to each nested script's own name) to recognise "the newest heading is the release currently being prepared" as distinct from a genuinely stale or wrong one — which a plain `npm run check`, CI, or a stray manual edit (none of which set that variable) still catch exactly as strictly as before. Verified end to end in an isolated throwaway clone: `npm version minor` ran preversion (full check + all 664 tests), bumped to 0.25.0, passed the post-bump changelog check, and tagged `v0.25.0` correctly.
58
+
25
59
  ## [0.24.3] — 2026-08-25
26
60
 
27
61
  ### Fixed
@@ -1 +1 @@
1
- import{buildCapabilityData as ct}from"../../stats/capability.js";import{capabilityColor as mt}from"./palette.js";import{CLR as V}from"../toolbar.js";import{cardShell as pt,chartFillHeight as dt,applyCanvasFlow as ht,observeResize as ut,makeTooltip as ft,positionChartTooltip as yt,makeLabeledSelect as bt,renderEmptyState as xt,resolveChartCanvasColors as vt}from"./chartShell.js";import{fmt as gt}from"../../renderer/fmt.js";const Mt=30,kt=160,Y=90,J=12;function A(M){return M===null?"\u2014":M.toFixed(2)}export function renderCapabilityPanel(M){const{title:K="Process capability",items:Z,testDefs:E,onSaveImage:tt,onSelectTest:W,groups:d}=M,{card:s,body:h,controlsRow:et}=pt(K,tt,M.ownerDocument),nt=E.some(e=>e.testNumber!==void 0);h.style.overflowX="auto";let H=d&&d.length>0?d[0].key:void 0;d&&d.length>0&&et.appendChild(bt("Group:",d.map(e=>({value:e.key,label:e.key})),H??"",e=>{H=e,O()},{ownerDocument:s.ownerDocument}));function ot(){return!d||d.length===0?Z:d.find(e=>e.key===H)?.items??[]}const R=s.ownerDocument.createElement("div");Object.assign(R.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),s.insertBefore(R,h);const k=window.devicePixelRatio||1,S=ft(s);let B=()=>{};function lt(e,n,u){R.innerHTML="";const m=s.ownerDocument.createElement("span");Object.assign(m.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:V.value,fontSize:"12px",fontWeight:"500"});const C=u-e,T=n>0?` \xB7 ${n} without spec limits`:"",L=s.ownerDocument.createElement("span");L.textContent=C>0?`${e} of ${u} tests shown${T} \xB7 ${C} excluded (no recorded values)`:`${e} test${e!==1?"s":""} shown${T}`,m.appendChild(L);const $=s.ownerDocument.createElement("span");$.textContent="\u24D8",$.title="Spec-limited tests are normalized so LSL=0 and USL=1, sorted worst Ppk first. Tests without both spec limits (muted, dashed) are normalized to their own observed range, sorted most-variable first. Box colour: green Ppk \u2265 1.33 (capable), orange Ppk \u2265 1.0 (marginal), red Ppk < 1.0 (poor).",Object.assign($.style,{color:V.label,cursor:"help",fontSize:"12px"}),m.appendChild($),R.appendChild(m)}function it(e){if(h.innerHTML="",e.length===0)return xt(h,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const n=s.ownerDocument.createElement("canvas");n.style.display="block",n.style.cursor="default",h.appendChild(n);const u=e.length,m=Math.min(0,...e.map(a=>a.min)),C=Math.max(1,...e.map(a=>a.max)),T=(C-m)*.05||.1,L=m-T,st=C+T-L||1;function X(a){return Math.max(Mt,Math.min(kt,Math.floor(a/u)))}function b(a,o,l){return o+(1-(a-L)/st)*l}let w=-1;function N(){ht(n);const a=h.clientWidth,o=X(a),l=o*u,i=J,x=l,f=dt(s,h,n,J+200+Y),_=Math.max(i+60,f-Y),y=_-i;n.width=Math.max(1,Math.floor(x*k)),n.height=Math.max(1,Math.floor(f*k)),n.style.width=`${x}px`,n.style.height=`${f}px`;const t=n.getContext("2d");t.setTransform(k,0,0,k,0,0),t.clearRect(0,0,x,f);const v=vt(s);e.forEach((c,j)=>{const g=j*o,r=g+o/2,p=Math.max(4,o*.55);if(j===w&&(t.fillStyle=v.bgHover,t.fillRect(g,i,o,_-i)),c.hasSpec){const q=b(0,i,y),Q=b(1,i,y);t.strokeStyle=v.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(g,q),t.lineTo(g+o,q),t.moveTo(g,Q),t.lineTo(g+o,Q),t.stroke(),t.setLineDash([])}const G=c.hasSpec?mt(c.ppk):v.textMuted,z=b(c.min,i,y),P=b(c.q1,i,y),U=b(c.median,i,y),D=b(c.q3,i,y),I=b(c.max,i,y);t.strokeStyle=v.textMuted,t.lineWidth=1,t.beginPath(),t.moveTo(r,z),t.lineTo(r,P),t.moveTo(r,D),t.lineTo(r,I),t.moveTo(r-p/4,z),t.lineTo(r+p/4,z),t.moveTo(r-p/4,I),t.lineTo(r+p/4,I),t.stroke(),t.fillStyle=G,t.globalAlpha=.5,t.fillRect(r-p/2,Math.min(P,D),p,Math.max(1,Math.abs(D-P))),t.globalAlpha=1,t.strokeStyle=G,c.hasSpec||t.setLineDash([4,2]),t.strokeRect(r-p/2,Math.min(P,D),p,Math.max(1,Math.abs(D-P))),t.setLineDash([]),t.strokeStyle=v.text,t.lineWidth=2,t.beginPath(),t.moveTo(r-p/2,U),t.lineTo(r+p/2,U),t.stroke(),t.lineWidth=1;const rt=c.label.length>12?`${c.label.slice(0,11)}\u2026`:c.label;t.save(),t.translate(r,_+6),t.rotate(-Math.PI/4),t.textAlign="right",t.textBaseline="middle",t.font="10px system-ui, sans-serif",t.fillStyle=v.textMuted,t.fillText(rt,0,0),t.restore()}),t.strokeStyle=v.border,t.lineWidth=1,t.strokeRect(0,i,l,y)}function F(a){const o=n.getBoundingClientRect(),l=h.clientWidth,i=X(l),x=(a.clientX-o.left)*(n.width/k/o.width),f=Math.floor(x/i);return f>=0&&f<u?f:-1}return n.addEventListener("mousemove",a=>{const o=F(a);if(o!==w&&(w=o,N()),o===-1){S.style.display="none";return}const l=e[o],i=x=>gt(x,l.unit);S.innerHTML=`<strong>${l.label}</strong> (n=${l.n})<br>`+(l.hasSpec?`LSL ${i(l.lsl)} \xB7 USL ${i(l.usl)}<br>mean ${i(l.mean)}<br>Cp ${A(l.cp)} \xB7 Cpk ${A(l.cpk)}<br>Pp ${A(l.pp)} \xB7 Ppk ${A(l.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${i(l.mean)} \xB7 stddev ${i(l.stdOverall)}`)+(W?"<br><em>click to view in boxplot</em>":""),S.style.display="block",yt(S,s,a.clientX,a.clientY),n.style.cursor=W?"pointer":"default"}),n.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,N()),S.style.display="none"}),n.addEventListener("click",a=>{if(!W)return;const o=F(a);o!==-1&&W(e[o].testNumber)}),N}function O(){const e=ct(ot(),E),n=E.filter(m=>m.testNumber!==void 0).length,u=e.filter(m=>!m.hasSpec).length;lt(e.length,u,n),B=it(e),B()}const at=ut(s,()=>B());return O(),{card:s,hasData:nt,destroy:()=>at.disconnect()}}
1
+ import{buildCapabilityData as he}from"../../stats/capability.js";import{capabilityColor as O}from"./palette.js";import{CLR as _}from"../toolbar.js";import{cardShell as fe,chartFillHeight as ue,applyCanvasFlow as be,observeResize as xe,makeTooltip as ye,positionChartTooltip as ge,makeLabeledSelect as ve,renderEmptyState as Me,resolveChartCanvasColors as Se}from"./chartShell.js";import{fmt as ke}from"../../renderer/fmt.js";const Te=30,Le=160,Z=90,ee=28,v=42;function I($){return $===null?"\u2014":$.toFixed(2)}export function renderCapabilityPanel($){const{title:te="Process capability",items:ne,testDefs:H,onSaveImage:oe,onSelectTest:B,groups:x}=$,{card:c,body:M,controlsRow:le}=fe(te,oe,$.ownerDocument),ie=H.some(n=>n.testNumber!==void 0);M.style.overflowX="auto";let j=x&&x.length>0?x[0].key:void 0;x&&x.length>0&&le.appendChild(ve("Group:",x.map(n=>({value:n.key,label:n.key})),j??"",n=>{j=n,q()},{ownerDocument:c.ownerDocument}));function se(){return!x||x.length===0?ne:x.find(n=>n.key===j)?.items??[]}const C=c.ownerDocument.createElement("div");Object.assign(C.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),c.insertBefore(C,M);const W=window.devicePixelRatio||1,A=ye(c);let z=()=>{};function ae(n,i,u){C.innerHTML="";const d=c.ownerDocument.createElement("span");Object.assign(d.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:_.value,fontSize:"12px",fontWeight:"500"});const S=u-n,m=i>0?` \xB7 ${i} without spec limits`:"",y=c.ownerDocument.createElement("span");y.textContent=S>0?`${n} of ${u} tests shown${m} \xB7 ${S} excluded (no recorded values)`:`${n} test${n!==1?"s":""} shown${m}`,d.appendChild(y),C.appendChild(d);const w=i>=n,k=c.ownerDocument.createElement("div");k.textContent=w?"No test has both spec limits, so each is normalised to its own observed range (min = 0, max = 1) and sorted most-variable first. Ppk needs limits, so none is shown.":"Normalised to spec limits (LSL = 0, USL = 1), worst Ppk first. Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(k.style,{color:_.label,fontSize:"11px",lineHeight:"1.45",marginTop:"3px",maxWidth:"78ch"}),C.appendChild(k),C.appendChild(re(c.ownerDocument,i>0,w))}function re(n,i,u=!1){const d=n.createElement("div");Object.assign(d.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:"4px 14px",marginTop:"7px",fontSize:"11px",color:_.label});const S=u?[]:[{color:O(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:O(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:O(.5),label:"Poor \xB7 Ppk < 1.0"}];i&&S.push({color:_.label,label:"No spec limits \xB7 no Ppk",dashed:!0});for(const m of S){const y=n.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:"5px"});const w=n.createElement("span");Object.assign(w.style,{width:"11px",height:"11px",flexShrink:"0",borderRadius:"2px",background:m.dashed?"transparent":m.color,border:`1px ${m.dashed?"dashed":"solid"} ${m.color}`,opacity:m.dashed?"1":"0.75"});const k=n.createElement("span");k.textContent=m.label,y.append(w,k),d.appendChild(y)}return d}function ce(n){if(M.innerHTML="",n.length===0)return Me(M,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const i=c.ownerDocument.createElement("canvas");i.style.display="block",i.style.cursor="default",M.appendChild(i);const u=n.length,d=Math.min(0,...n.map(t=>t.min)),S=Math.max(1,...n.map(t=>t.max)),m=(S-d)*.05||.1,y=d-m,k=S+m-y||1;function Q(t){return Math.max(Te,Math.min(Le,Math.floor(t/u)))}function T(t,s,o){return s+(1-(t-y)/k)*o}function pe(t,s,o,l,g){const h=n.filter(P=>P.hasSpec).length,E=h===n.length,p=h===0,e=p?"max":"USL",f=p?"min":"LSL",r=E?"normalised to spec":p?"normalised to range":"normalised (per test)";t.font="10px system-ui, sans-serif",t.textAlign="right",t.textBaseline="middle";for(const[P,L]of[[1,e],[0,f]]){const a=T(P,o,l);a<o-1||a>o+l+1||(t.strokeStyle=s.warnBorder,t.lineWidth=1,t.beginPath(),t.moveTo(v-5,a),t.lineTo(v,a),t.stroke(),t.fillStyle=s.textMuted,t.fillText(L,v-8,a))}t.save(),t.translate(9,o+l/2),t.rotate(-Math.PI/2),t.textAlign="center",t.fillStyle=s.textMuted,t.fillText(r,0,0),t.restore()}let D=-1;function X(){be(i);const t=M.clientWidth,s=Q(t-v),o=s*u,l=ee,g=v+o,h=ue(c,M,i,ee+200+Z),E=Math.max(l+60,h-Z),p=E-l;i.width=Math.max(1,Math.floor(g*W)),i.height=Math.max(1,Math.floor(h*W)),i.style.width=`${g}px`,i.style.height=`${h}px`;const e=i.getContext("2d");e.setTransform(W,0,0,W,0,0),e.clearRect(0,0,g,h);const f=Se(c);n.forEach((r,P)=>{const L=v+P*s,a=L+s/2,b=Math.max(4,s*.55);if(P===D&&(e.fillStyle=f.bgHover,e.fillRect(L,l,s,E-l)),r.hasSpec){const J=T(0,l,p),K=T(1,l,p);e.strokeStyle=f.warnBorder,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(L,J),e.lineTo(L+s,J),e.moveTo(L,K),e.lineTo(L+s,K),e.stroke(),e.setLineDash([])}const U=r.hasSpec?O(r.ppk):f.textMuted,F=T(r.min,l,p),R=T(r.q1,l,p),Y=T(r.median,l,p),N=T(r.q3,l,p),G=T(r.max,l,p);e.strokeStyle=f.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(a,F),e.lineTo(a,R),e.moveTo(a,N),e.lineTo(a,G),e.moveTo(a-b/4,F),e.lineTo(a+b/4,F),e.moveTo(a-b/4,G),e.lineTo(a+b/4,G),e.stroke(),e.fillStyle=U,e.globalAlpha=.5,e.fillRect(a-b/2,Math.min(R,N),b,Math.max(1,Math.abs(N-R))),e.globalAlpha=1,e.strokeStyle=U,r.hasSpec||e.setLineDash([4,2]),e.strokeRect(a-b/2,Math.min(R,N),b,Math.max(1,Math.abs(N-R))),e.setLineDash([]),e.strokeStyle=f.text,e.lineWidth=2,e.beginPath(),e.moveTo(a-b/2,Y),e.lineTo(a+b/2,Y),e.stroke(),e.lineWidth=1,e.font="10px system-ui, sans-serif",e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=r.hasSpec?U:f.textMuted,e.fillText(r.hasSpec?I(r.ppk):"\u2014",a,l-8);const me=r.label.length>12?`${r.label.slice(0,11)}\u2026`:r.label;e.save(),e.translate(a,E+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font="10px system-ui, sans-serif",e.fillStyle=f.textMuted,e.fillText(me,0,0),e.restore()}),e.strokeStyle=f.border,e.lineWidth=1,e.strokeRect(v,l,o,p),pe(e,f,l,p,o)}function V(t){const s=i.getBoundingClientRect(),o=M.clientWidth,l=Q(o-v),g=(t.clientX-s.left)*(i.width/W/s.width)-v,h=Math.floor(g/l);return h>=0&&h<u?h:-1}return i.addEventListener("mousemove",t=>{const s=V(t);if(s!==D&&(D=s,X()),s===-1){A.style.display="none";return}const o=n[s],l=g=>ke(g,o.unit);A.innerHTML=`<strong>${o.label}</strong> (n=${o.n})<br>`+(o.hasSpec?`LSL ${l(o.lsl)} \xB7 USL ${l(o.usl)}<br>mean ${l(o.mean)}<br>Cp ${I(o.cp)} \xB7 Cpk ${I(o.cpk)}<br>Pp ${I(o.pp)} \xB7 Ppk ${I(o.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${l(o.mean)} \xB7 stddev ${l(o.stdOverall)}`)+(B?"<br><em>click to view in boxplot</em>":""),A.style.display="block",ge(A,c,t.clientX,t.clientY),i.style.cursor=B?"pointer":"default"}),i.addEventListener("mouseleave",()=>{D!==-1&&(D=-1,X()),A.style.display="none"}),i.addEventListener("click",t=>{if(!B)return;const s=V(t);s!==-1&&B(n[s].testNumber)}),X}function q(){const n=he(se(),H),i=H.filter(d=>d.testNumber!==void 0).length,u=n.filter(d=>!d.hasSpec).length;ae(n.length,u,i),z=ce(n),z()}const de=xe(c,()=>z());return q(),{card:c,hasData:ie,destroy:()=>de.disconnect()}}
@@ -149,6 +149,22 @@ export declare function makeLabeledSelect(labelText: string, options: readonly {
149
149
  export declare function makeChartGridWrap(ownerDocument?: Document): HTMLDivElement;
150
150
  /** Small positioned hover tooltip, matching the pattern each ported panel builds for itself. */
151
151
  export declare function makeTooltip(card: HTMLElement): HTMLElement;
152
+ /**
153
+ * Give a chart-card control the library's themed hover tooltip instead of a
154
+ * native `title`.
155
+ *
156
+ * toolbar.ts states the policy for the map toolbar — `ariaLabel` plus the
157
+ * custom tooltip, deliberately no `title`, because a native tooltip is slow,
158
+ * unthemeable, invisible on touch and unreachable by keyboard. The chart layer
159
+ * never adopted it and kept setting `title` directly, so hints that make an
160
+ * interaction discoverable ("click to filter") were reachable only by hovering
161
+ * with a mouse and knowing to hover in the first place.
162
+ *
163
+ * `ariaLabel` is set only when the element has no text of its own — on a
164
+ * labelled element it would override the visible name for screen readers,
165
+ * which is worse than the tooltip it replaces.
166
+ */
167
+ export declare function attachChartTip(el: HTMLElement, card: HTMLElement, tooltip: HTMLElement, text: string): void;
152
168
  /**
153
169
  * Position a `makeTooltip` element from a mousemove event, flipping to the
154
170
  * opposite side of the cursor when the default placement would overflow
@@ -1 +1 @@
1
- import{CLR as a,Z_BASE as T,MENU_SEARCH_THRESHOLD as N,makeMenuSearchBox as M,markMenuTrigger as E,saveImageBlob as I,openReparentedModal as z}from"../toolbar.js";import{ICONS as O}from"../icons.js";import{fmt as W,fmtColorbarAxis as F}from"../../renderer/fmt.js";export function openChartExpandModal(e,o,r){const t=e.getAttribute("style")??"";if(Object.assign(e.style,{flex:"1",minHeight:"0",border:"none",borderRadius:"0"}),!z([e],{title:o,onClosed:()=>{e.setAttribute("style",t),r&&(r.style.display="flex")}})){e.setAttribute("style",t);return}r&&(r.style.display="none")}const P={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040"},j={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border"};export function resolveChartCanvasColors(e){const o=getComputedStyle(e),r={};for(const t of Object.keys(j)){const i=o.getPropertyValue(`--wmap-${j[t]}`).trim();r[t]=i||P[t]}return r}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,o){const r=new ResizeObserver(o);return r.observe(e),{disconnect:()=>r.disconnect()}}export function growCardToFitContent(e,o,r){const t=e.offsetHeight-o.clientHeight;e.style.minHeight=`${t+r}px`}export function chartFillHeight(e,o,r,t){let i=0;for(const s of Array.from(o.children))s!==r&&(i+=s.offsetHeight);return growCardToFitContent(e,o,i+t),Math.max(t,o.clientHeight-i)}export function applyCanvasFlow(e,o=0){e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${o}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:W(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,o){if(!o)return{tick:formatValue,unitLabel:""};const{tickFmt:r,axisLabel:t}=F(e,null,o);return{tick:r,unitLabel:t}}export function drawAxisUnit(e,o,r,t,i){const s={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font="10px system-ui, sans-serif",e.textAlign="left",e.textBaseline="middle",e.fillStyle=i,e.fillText(`(${o})`,r,t),Object.assign(e,s)}export function saveCanvasPng(e,o,r){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(s=>{s&&I(s,o,r)},"image/png")}export function cardShell(e,o,r=document){const t=r.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"6px",padding:"12px",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:"6px",marginBottom:"6px"});const s=t.ownerDocument.createElement("div");s.textContent=e,Object.assign(s.style,{color:a.value,fontSize:"13px",fontWeight:"600",flex:"1"}),i.appendChild(s);const l=t.ownerDocument.createElement("button");l.type="button",l.title="Save as PNG",l.setAttribute("aria-label","Save as PNG"),l.innerHTML=O.download,Object.assign(l.style,{border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:"none",color:a.label,cursor:"pointer",width:"22px",height:"22px",lineHeight:"1",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),l.addEventListener("click",()=>{const u=t.querySelector("canvas");u&&saveCanvasPng(u,e,o)}),i.appendChild(l);const n=t.ownerDocument.createElement("button");n.type="button",n.title="Expand",n.setAttribute("aria-label","Expand"),n.innerHTML=O.expand,Object.assign(n.style,{border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:"none",color:a.label,cursor:"pointer",width:"22px",height:"22px",lineHeight:"1",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),n.addEventListener("click",()=>openChartExpandModal(t,s.textContent??e,n)),i.appendChild(n),t.appendChild(i);const d=t.ownerDocument.createElement("div");Object.assign(d.style,{display:"flex",gap:"6px",marginBottom:"6px",flexWrap:"wrap",alignItems:"center"}),t.appendChild(d);const p=t.ownerDocument.createElement("div");return Object.assign(p.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(p),{card:t,heading:s,controlsRow:d,body:p}}export function makeSegmented(e,o,r,t=document){const i=t.createElement("div");i.setAttribute("role","radiogroup"),Object.assign(i.style,{display:"inline-flex",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",overflow:"hidden"});const s=`seg-${Math.random().toString(36).slice(2,9)}`,l=[];return e.forEach(([n,d],p)=>{const u=t.createElement("label");Object.assign(u.style,{display:"inline-flex",alignItems:"center",fontSize:"12px",padding:"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:p>0?`1px solid ${a.menuBorder}`:"none"});const c=t.createElement("input");c.type="radio",c.name=s,c.value=n,c.checked=n===o,Object.assign(c.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const h=()=>{u.style.background=c.checked?a.bgActive:a.menuBg,u.style.color=c.checked?a.iconActive:a.text};l.push(h),h(),c.addEventListener("change",()=>{if(c.checked){for(const y of l)y();r(n)}}),u.append(c,t.createTextNode(d)),i.appendChild(u)}),i}export function makeBackButton(e,o=document){const r=o.createElement("button");return r.type="button",r.textContent="\u2190 Back",Object.assign(r.style,{fontSize:"12px",padding:"3px 10px",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:a.menuBg,color:a.label,cursor:"pointer"}),r.addEventListener("click",e),r}export function renderEmptyState(e,o,r){const t=e.ownerDocument.createElement("div");t.textContent=o,Object.assign(t.style,{color:a.label,fontSize:"12px",padding:"8px 0"}),r&&Object.assign(t.style,r),e.appendChild(t)}export function makeTestSelect(e,o,r,t={}){const{maxWidth:i="200px",emptyText:s="No parametric tests",ownerDocument:l=document}=t;if(e.length>N)return _(e,o,r,i,l);const n=l.createElement("select");if(Object.assign(n.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:i}),e.length===0){n.disabled=!0;const d=l.createElement("option");d.textContent=s,n.appendChild(d)}else{for(const d of e){const p=l.createElement("option");p.value=String(d.testNumber),p.textContent=d.name||`Test ${d.testNumber}`,d.testNumber===o&&(p.selected=!0),n.appendChild(p)}n.addEventListener("change",()=>r(Number(n.value)))}return n}function _(e,o,r,t,i=document){const s=x=>e.find(v=>v.testNumber===x)?.name||`Test ${x}`;let l=o;const n=i.createElement("button");n.type="button",Object.assign(n.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:t,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:"6px",justifyContent:"space-between"}),E(n,!1);const d=n.ownerDocument.createElement("span");Object.assign(d.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const p=n.ownerDocument.createElement("span");p.textContent="\u25BE",p.style.flex="0 0 auto",n.append(d,p);const u=()=>{d.textContent=l!==null?s(l):"(none)"};u();let c=null;const h=()=>{c&&(c.remove(),c=null,E(n,!1))},y=()=>{h(),n.focus()};function D(){const x=n.getBoundingClientRect(),v=n.ownerDocument.defaultView??window,S=Math.max(x.width,220),R=320,$=Math.min(x.left,Math.max(4,(v.innerWidth??1/0)-S-4));c=n.ownerDocument.createElement("div"),Object.assign(c.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${$}px`,background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",boxShadow:"0 4px 12px rgba(0,0,0,0.15)",zIndex:T,minWidth:`${S}px`,maxHeight:`${R}px`,overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const w=[],B=()=>w.filter(f=>f.row.style.display!=="none").map(f=>f.row),k=M(f=>{for(const m of w)m.row.style.display=m.label.includes(f)?"":"none"},"Filter tests\u2026",n.ownerDocument);Object.assign(k.style,{position:"sticky",top:"0",zIndex:"1",background:a.menuBg}),k.addEventListener("keydown",f=>{if(f.key==="Escape"){y();return}f.key==="ArrowDown"&&(f.preventDefault(),B()[0]?.focus())}),c.appendChild(k);for(const f of e){const m=f.name||`Test ${f.testNumber}`,b=f.testNumber===l,g=n.ownerDocument.createElement("div");g.textContent=m,g.setAttribute("role","menuitemradio"),g.setAttribute("aria-checked",b?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:"6px 14px",fontSize:"12px",cursor:"pointer",color:b?a.iconActive:a.text,fontWeight:b?"700":"400",background:b?a.menuActive:"transparent",whiteSpace:"nowrap",outline:"none"});const A=()=>{b||(g.style.background=a.menuHover)},L=()=>{g.style.background=b?a.menuActive:"transparent"};g.addEventListener("mouseenter",A),g.addEventListener("mouseleave",L),g.addEventListener("focus",A),g.addEventListener("blur",L),g.addEventListener("click",H=>{H.stopPropagation(),l=f.testNumber,u(),y(),r(f.testNumber)}),c.appendChild(g),w.push({row:g,label:m.toLowerCase()})}c.addEventListener("keydown",f=>{const m=B();if(m.length===0)return;const b=m.indexOf(n.ownerDocument.activeElement);switch(f.key){case"ArrowDown":f.preventDefault(),m[b<0||b===m.length-1?0:b+1].focus();break;case"ArrowUp":f.preventDefault(),m[b<=0?m.length-1:b-1].focus();break;case"Home":f.preventDefault(),m[0].focus();break;case"End":f.preventDefault(),m[m.length-1].focus();break;case"Enter":case" ":b>=0&&(f.preventDefault(),m[b].click());break;case"Escape":case"Tab":f.preventDefault(),y();break}}),c.setAttribute("role","menu"),c.setAttribute("aria-label","Test"),n.ownerDocument.body.appendChild(c),E(n,!0)}n.addEventListener("click",x=>{if(x.stopPropagation(),c){h();return}D()});const C=x=>{if(!n.isConnected){n.ownerDocument.removeEventListener("click",C);return}c&&!c.contains(x.target)&&!n.contains(x.target)&&h()};return n.ownerDocument.addEventListener("click",C),Object.defineProperty(n,"value",{get:()=>l!==null?String(l):"",set:x=>{l=x===""?null:Number(x),u()}})}export function makeWaferSelect(e,o,r,t={}){const{maxWidth:i="160px",allLabel:s="All wafers",ownerDocument:l=document}=t,n="\0all",d=l.createElement("select");Object.assign(d.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:i});const p=l.createElement("option");return p.value=n,p.textContent=s,o===null&&(p.selected=!0),d.appendChild(p),e.forEach((u,c)=>{const h=l.createElement("option");h.value=String(c),h.textContent=u.label??`#${c}`,o===c&&(h.selected=!0),d.appendChild(h)}),d.addEventListener("change",()=>r(d.value===n?null:Number(d.value))),d}export function makeToggle(e,o,r,t=document){const i=t.createElement("label");Object.assign(i.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:a.label,cursor:"pointer",userSelect:"none"});const s=t.createElement("input");return s.type="checkbox",s.checked=o,s.style.cssText="margin:0;cursor:pointer;",s.addEventListener("change",()=>r(s.checked)),i.append(s,t.createTextNode(e)),i}export function makeLabeledSelect(e,o,r,t,i={}){const{maxWidth:s="160px",hook:l,ownerDocument:n=document}=i,d=n.createElement("label");d.textContent=e,Object.assign(d.style,{color:a.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const p=n.createElement("select");l&&(p.dataset.wmapSelect=l),Object.assign(p.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:s});for(const u of o){const c=n.createElement("option");c.value=u.value,c.textContent=u.label,u.value===r&&(c.selected=!0),p.appendChild(c)}return p.addEventListener("change",()=>t(p.value)),d.appendChild(p),d}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:"10px",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:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",fontFamily:"system-ui, sans-serif",color:a.text,whiteSpace:"nowrap",boxShadow:"0 2px 8px rgba(0,0,0,0.3)"}),e.appendChild(o),o}export function positionChartTooltip(e,o,r,t){const i=o.getBoundingClientRect(),s=14,l=4;e.style.left="0",e.style.top="0";const n=e.offsetWidth,d=e.offsetHeight;let p=r-i.left+s,u=t-i.top+s;p+n+l>i.width&&(p=r-i.left-s-n),u+d+l>i.height&&(u=t-i.top-s-d),p=Math.max(l,p),u=Math.max(l,u),e.style.left=`${p}px`,e.style.top=`${u}px`}
1
+ import{CLR as a,Z_BASE as H,menuLayerFor as N,MENU_SEARCH_THRESHOLD as M,makeMenuSearchBox as I,markMenuTrigger as k,saveImageBlob as z,openReparentedModal as W}from"../toolbar.js";import{ICONS as O}from"../icons.js";import{fmt as F,fmtColorbarAxis as P}from"../../renderer/fmt.js";export function openChartExpandModal(e,r,n){const t=e.getAttribute("style")??"";if(Object.assign(e.style,{flex:"1",minHeight:"0",border:"none",borderRadius:"0"}),!W([e],{title:r,onClosed:()=>{e.setAttribute("style",t),n&&(n.style.display="flex")}})){e.setAttribute("style",t);return}n&&(n.style.display="none")}const _={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040"},j={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border"};export function resolveChartCanvasColors(e){const r=getComputedStyle(e),n={};for(const t of Object.keys(j)){const o=r.getPropertyValue(`--wmap-${j[t]}`).trim();n[t]=o||_[t]}return n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,r){const n=e.ownerDocument.defaultView??window;let t=0;const o=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,r()}))});return o.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,o.disconnect()}}}export function growCardToFitContent(e,r,n){const o=`${e.offsetHeight-r.clientHeight+n}px`;e.style.minHeight!==o&&(e.style.minHeight=o)}export function chartFillHeight(e,r,n,t){let o=0;for(const c of Array.from(r.children))c!==n&&(o+=c.offsetHeight);return growCardToFitContent(e,r,o+t),Math.max(t,r.clientHeight-o)}export function applyCanvasFlow(e,r=0){e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${r}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:F(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,r){if(!r)return{tick:formatValue,unitLabel:""};const{tickFmt:n,axisLabel:t}=P(e,null,r);return{tick:n,unitLabel:t}}export function drawAxisUnit(e,r,n,t,o){const c={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font="10px system-ui, sans-serif",e.textAlign="left",e.textBaseline="middle",e.fillStyle=o,e.fillText(`(${r})`,n,t),Object.assign(e,c)}export function saveCanvasPng(e,r,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(c=>{c&&z(c,r,n)},"image/png")}export function cardShell(e,r,n=document){const t=n.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"6px",padding:"12px",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:"6px",marginBottom:"6px"});const c=t.ownerDocument.createElement("div");c.textContent=e,Object.assign(c.style,{color:a.value,fontSize:"13px",fontWeight:"600",flex:"1"}),o.appendChild(c);const p=makeTooltip(t),i=t.ownerDocument.createElement("button");i.type="button",i.setAttribute("aria-label","Save as PNG"),i.innerHTML=O.download,Object.assign(i.style,{border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:"none",color:a.label,cursor:"pointer",width:"22px",height:"22px",lineHeight:"1",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),attachChartTip(i,t,p,"Save as PNG"),i.addEventListener("click",()=>{const l=t.querySelector("canvas");l&&saveCanvasPng(l,e,r)}),o.appendChild(i);const s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Expand"),s.innerHTML=O.expand,Object.assign(s.style,{border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:"none",color:a.label,cursor:"pointer",width:"22px",height:"22px",lineHeight:"1",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),attachChartTip(s,t,p,"Expand"),s.addEventListener("click",()=>openChartExpandModal(t,c.textContent??e,s)),o.appendChild(s),t.appendChild(o);const d=t.ownerDocument.createElement("div");Object.assign(d.style,{display:"flex",gap:"6px",marginBottom:"6px",flexWrap:"wrap",alignItems:"center"}),t.appendChild(d);const u=t.ownerDocument.createElement("div");return Object.assign(u.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(u),{card:t,heading:c,controlsRow:d,body:u}}export function makeSegmented(e,r,n,t=document){const o=t.createElement("div");o.setAttribute("role","radiogroup"),Object.assign(o.style,{display:"inline-flex",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",overflow:"hidden"});const c=`seg-${Math.random().toString(36).slice(2,9)}`,p=[];return e.forEach(([i,s],d)=>{const u=t.createElement("label");Object.assign(u.style,{display:"inline-flex",alignItems:"center",fontSize:"12px",padding:"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:d>0?`1px solid ${a.menuBorder}`:"none"});const l=t.createElement("input");l.type="radio",l.name=c,l.value=i,l.checked=i===r,Object.assign(l.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const h=()=>{u.style.background=l.checked?a.bgActive:a.menuBg,u.style.color=l.checked?a.iconActive:a.text};p.push(h),h(),l.addEventListener("change",()=>{if(l.checked){for(const y of p)y();n(i)}}),u.append(l,t.createTextNode(s)),o.appendChild(u)}),o}export function makeBackButton(e,r=document){const n=r.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{fontSize:"12px",padding:"3px 10px",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:a.menuBg,color:a.label,cursor:"pointer"}),n.addEventListener("click",e),n}export function renderEmptyState(e,r,n){const t=e.ownerDocument.createElement("div");t.textContent=r,Object.assign(t.style,{color:a.label,fontSize:"12px",padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeTestSelect(e,r,n,t={}){const{maxWidth:o="200px",emptyText:c="No parametric tests",ownerDocument:p=document}=t;if(e.length>M)return G(e,r,n,o,p);const i=p.createElement("select");if(Object.assign(i.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:o}),e.length===0){i.disabled=!0;const s=p.createElement("option");s.textContent=c,i.appendChild(s)}else{for(const s of e){const d=p.createElement("option");d.value=String(s.testNumber),d.textContent=s.name||`Test ${s.testNumber}`,s.testNumber===r&&(d.selected=!0),i.appendChild(d)}i.addEventListener("change",()=>n(Number(i.value)))}return i}function G(e,r,n,t,o=document){const c=x=>e.find(v=>v.testNumber===x)?.name||`Test ${x}`;let p=r;const i=o.createElement("button");i.type="button",Object.assign(i.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:t,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:"6px",justifyContent:"space-between"}),k(i,!1);const s=i.ownerDocument.createElement("span");Object.assign(s.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const d=i.ownerDocument.createElement("span");d.textContent="\u25BE",d.style.flex="0 0 auto",i.append(s,d);const u=()=>{s.textContent=p!==null?c(p):"(none)"};u();let l=null;const h=()=>{l&&(l.remove(),l=null,k(i,!1))},y=()=>{h(),i.focus()};function D(){const x=i.getBoundingClientRect(),v=i.ownerDocument.defaultView??window,S=Math.max(x.width,220),R=320,T=Math.min(x.left,Math.max(4,(v.innerWidth??1/0)-S-4));l=i.ownerDocument.createElement("div"),Object.assign(l.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${T}px`,background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",boxShadow:"0 4px 12px rgba(0,0,0,0.15)",zIndex:H,minWidth:`${S}px`,maxHeight:`${R}px`,overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const w=[],L=()=>w.filter(f=>f.row.style.display!=="none").map(f=>f.row),E=I(f=>{for(const m of w)m.row.style.display=m.label.includes(f)?"":"none"},"Filter tests\u2026",i.ownerDocument);Object.assign(E.style,{position:"sticky",top:"0",zIndex:"1",background:a.menuBg}),E.addEventListener("keydown",f=>{if(f.key==="Escape"){y();return}f.key==="ArrowDown"&&(f.preventDefault(),L()[0]?.focus())}),l.appendChild(E);for(const f of e){const m=f.name||`Test ${f.testNumber}`,b=f.testNumber===p,g=i.ownerDocument.createElement("div");g.textContent=m,g.setAttribute("role","menuitemradio"),g.setAttribute("aria-checked",b?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:"6px 14px",fontSize:"12px",cursor:"pointer",color:b?a.iconActive:a.text,fontWeight:b?"700":"400",background:b?a.menuActive:"transparent",whiteSpace:"nowrap",outline:"none"});const A=()=>{b||(g.style.background=a.menuHover)},B=()=>{g.style.background=b?a.menuActive:"transparent"};g.addEventListener("mouseenter",A),g.addEventListener("mouseleave",B),g.addEventListener("focus",A),g.addEventListener("blur",B),g.addEventListener("click",$=>{$.stopPropagation(),p=f.testNumber,u(),y(),n(f.testNumber)}),l.appendChild(g),w.push({row:g,label:m.toLowerCase()})}l.addEventListener("keydown",f=>{const m=L();if(m.length===0)return;const b=m.indexOf(i.ownerDocument.activeElement);switch(f.key){case"ArrowDown":f.preventDefault(),m[b<0||b===m.length-1?0:b+1].focus();break;case"ArrowUp":f.preventDefault(),m[b<=0?m.length-1:b-1].focus();break;case"Home":f.preventDefault(),m[0].focus();break;case"End":f.preventDefault(),m[m.length-1].focus();break;case"Enter":case" ":b>=0&&(f.preventDefault(),m[b].click());break;case"Escape":case"Tab":f.preventDefault(),y();break}}),l.setAttribute("role","menu"),l.setAttribute("aria-label","Test"),N(i).appendChild(l),k(i,!0)}i.addEventListener("click",x=>{if(x.stopPropagation(),l){h();return}D()});const C=x=>{if(!i.isConnected){i.ownerDocument.removeEventListener("click",C);return}l&&!l.contains(x.target)&&!i.contains(x.target)&&h()};return i.ownerDocument.addEventListener("click",C),Object.defineProperty(i,"value",{get:()=>p!==null?String(p):"",set:x=>{p=x===""?null:Number(x),u()}})}export function makeWaferSelect(e,r,n,t={}){const{maxWidth:o="160px",allLabel:c="All wafers",ownerDocument:p=document}=t,i="\0all",s=p.createElement("select");Object.assign(s.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:o});const d=p.createElement("option");return d.value=i,d.textContent=c,r===null&&(d.selected=!0),s.appendChild(d),e.forEach((u,l)=>{const h=p.createElement("option");h.value=String(l),h.textContent=u.label??`#${l}`,r===l&&(h.selected=!0),s.appendChild(h)}),s.addEventListener("change",()=>n(s.value===i?null:Number(s.value))),s}export function makeToggle(e,r,n,t=document){const o=t.createElement("label");Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:a.label,cursor:"pointer",userSelect:"none"});const c=t.createElement("input");return c.type="checkbox",c.checked=r,c.style.cssText="margin:0;cursor:pointer;",c.addEventListener("change",()=>n(c.checked)),o.append(c,t.createTextNode(e)),o}export function makeLabeledSelect(e,r,n,t,o={}){const{maxWidth:c="160px",hook:p,ownerDocument:i=document}=o,s=i.createElement("label");s.textContent=e,Object.assign(s.style,{color:a.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const d=i.createElement("select");p&&(d.dataset.wmapSelect=p),Object.assign(d.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:c});for(const u of r){const l=i.createElement("option");l.value=u.value,l.textContent=u.label,u.value===n&&(l.selected=!0),d.appendChild(l)}return d.addEventListener("change",()=>t(d.value)),s.appendChild(d),s}export function makeChartGridWrap(e=document){const r=e.createElement("div");return r.dataset.wmapChartGrid="1",Object.assign(r.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:"10px",flex:"0 0 auto"}),r}export function makeTooltip(e){const r=e.ownerDocument.createElement("div");return Object.assign(r.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",fontFamily:"system-ui, sans-serif",color:a.text,whiteSpace:"nowrap",boxShadow:"0 2px 8px rgba(0,0,0,0.3)"}),e.appendChild(r),r}export function attachChartTip(e,r,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",o=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,r,o.clientX,o.clientY)}),e.addEventListener("mousemove",o=>{n.style.display==="block"&&positionChartTooltip(n,r,o.clientX,o.clientY)}),e.addEventListener("mouseleave",()=>{n.style.display="none"}),e.addEventListener("focus",()=>{n.textContent=t,n.style.display="block";const o=e.getBoundingClientRect();positionChartTooltip(n,r,o.left+o.width/2,o.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,r,n,t){const o=r.getBoundingClientRect(),c=14,p=4;e.style.left="0",e.style.top="0";const i=e.offsetWidth,s=e.offsetHeight;let d=n-o.left+c,u=t-o.top+c;d+i+p>o.width&&(d=n-o.left-c-i),u+s+p>o.height&&(u=t-o.top-c-s),d=Math.max(p,d),u=Math.max(p,u),e.style.left=`${d}px`,e.style.top=`${u}px`}
@@ -1 +1 @@
1
- import{buildCorrelationMatrix as ut,filterCorrelationMatrix as ft}from"../../stats/correlation.js";import{CORRELATION_POSITIVE as et,CORRELATION_NEGATIVE as nt}from"./palette.js";import{buildFacetTable as mt}from"../../stats/facets.js";import{CLR as g}from"../toolbar.js";import{cardShell as ht,observeResize as xt,makeTooltip as gt,positionChartTooltip as bt,makeLabeledSelect as yt,makeWaferSelect as Mt,renderEmptyState as wt,resolveChartCanvasColors as Lt}from"./chartShell.js";const ot=5,st=100,lt=20;function it(I){const d=I.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(d)return[+d[1],+d[2],+d[3]];const a=I.trim().replace("#","");return a.length===6?[parseInt(a.slice(0,2),16),parseInt(a.slice(2,4),16),parseInt(a.slice(4,6),16)]:null}function vt(I,d,a){const k=it(I);if(!k)return I;const P=Math.round(d[0]+(k[0]-d[0])*a),m=Math.round(d[1]+(k[1]-d[1])*a),D=Math.round(d[2]+(k[2]-d[2])*a);return`rgb(${P},${m},${D})`}export function renderCorrelationPanel(I){const{title:d="Test correlation matrix",items:a,testDefs:k,onSaveImage:P,groups:m,onSelectPair:D}=I,{card:l,body:$,controlsRow:V}=ht(d,P,I.ownerDocument);$.style.overflowX="auto",l.style.alignSelf="start";let F=lt,U=m&&m.length>0?m[0].key:void 0,N=null;const j=l.ownerDocument.createElement("label");j.textContent="Max tests:",j.title="Cap on how many tests the matrix includes (strongest correlations kept first)",Object.assign(j.style,{color:g.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const L=l.ownerDocument.createElement("input");L.type="number",L.min=String(ot),L.max=String(st),L.value=String(F),Object.assign(L.style,{width:"52px",fontSize:"12px",padding:"2px 4px",background:g.menuBg,color:g.value,border:`1px solid ${g.menuBorder}`,borderRadius:"3px"}),L.addEventListener("change",()=>{const e=Math.max(ot,Math.min(st,parseInt(L.value,10)||lt));L.value=String(e),e!==F&&(F=e,G())}),j.appendChild(L),V.appendChild(j),m&&m.length>0?V.appendChild(yt("Group:",m.map(e=>({value:e.key,label:e.key})),U??"",e=>{U=e,G()},{ownerDocument:l.ownerDocument})):a.length>1&&V.appendChild(Mt(a,N,e=>{N=e,G()},{ownerDocument:l.ownerDocument}));function rt(){return m&&m.length>0?m.find(e=>e.key===U)?.items??[]:N!==null?a[N]?[a[N]]:[]:a}const A=l.ownerDocument.createElement("div");Object.assign(A.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),l.insertBefore(A,$);const B=window.devicePixelRatio||1,S=gt(l);let q=()=>{};function at(e,s,c,f,C){if(A.innerHTML="",C.length>0){const y=l.ownerDocument.createElement("div");y.textContent=`\u26A0 Mixed ${C.join(", ")} within this set \u2014 correlations may be misleading (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,Object.assign(y.style,{color:g.warnText,background:g.warnBg,border:`1px solid ${g.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px"}),A.appendChild(y)}const r=l.ownerDocument.createElement("span");Object.assign(r.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:g.label,fontSize:"11px",flexWrap:"wrap"});const E=l.ownerDocument.createElement("span");E.textContent="Pearson r \xB7 click a cell to view that pair in scatter \xB7",r.appendChild(E);const p=l.ownerDocument.createElement("span");Object.assign(p.style,{display:"inline-flex",alignItems:"center",gap:"4px"});const W=l.ownerDocument.createElement("span");W.textContent="\u22121";const b=l.ownerDocument.createElement("span");Object.assign(b.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:"2px",border:`1px solid ${g.menuBorder}`,background:`linear-gradient(to right, ${nt}, ${g.menuBg}, ${et})`});const O=l.ownerDocument.createElement("span");O.textContent="+1",p.append(W,b,O),r.appendChild(p),A.appendChild(r);const T=l.ownerDocument.createElement("span");if(Object.assign(T.style,{color:g.value,fontSize:"12px",fontWeight:"500"}),e===0&&s===0)T.textContent=f?`No significant correlations found \u2014 strongest pair: ${f.xLabel} \u2194 ${f.yLabel} (r = ${f.r.toFixed(2)})`:"No significant correlations found";else{const y=[];e>0&&y.push(`${e} strong (|r| \u2265 0.7)`),s>0&&y.push(`${s} moderate (0.4\u20130.7)`);const H=e+s;let X=y.join(", ")+` pair${H!==1?"s":""} found`;c>0&&(X+=` \xB7 ${c} weak pair${c!==1?"s":""} not shown`),T.textContent=X}A.appendChild(T)}function ct(e){if($.innerHTML="",e.tests.length<2)return wt($,"Need at least two parametric tests for a correlation matrix."),()=>{};const s=l.ownerDocument.createElement("canvas");s.style.display="block",s.style.cursor="default",$.appendChild(s);const c=e.tests.length,f=o=>o.label.split(" (#")[0],C=Math.min(14,Math.max(...e.tests.map(o=>f(o).length))),r=C*6.5+8,E=10,p=Math.round(E*6.5*Math.sin(Math.PI/4))+14,W=14,b=26;let O=-1,T=-1;function y(o){const i=Math.max(0,o-r);return Math.max(W,Math.min(b,Math.floor(i/c)))}const H=new Map;for(const o of e.cells){let i=H.get(o.yIndex);i||(i=[],H.set(o.yIndex,i)),i.push(o)}function X(){const o=Lt(l),i=it(o.bg)??[255,255,255],u=$.clientWidth,n=y(u),_=n*c,v=p+n*c+4,M=r+_;s.width=Math.max(1,Math.floor(M*B)),s.height=Math.max(1,Math.floor(v*B)),s.style.width=`${M}px`,s.style.height=`${v}px`;const t=s.getContext("2d");t.setTransform(B,0,0,B,0,0),t.clearRect(0,0,M,v),t.font="10px system-ui, sans-serif",t.fillStyle=o.text,e.tests.forEach((h,x)=>{const w=f(h),R=w.length>E?`${w.slice(0,E-1)}\u2026`:w,J=r+x*n+n/2;t.save(),t.translate(J,p-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=x===O?o.text:o.textMuted,t.fillText(R,0,0),t.restore()}),e.tests.forEach((h,x)=>{const w=f(h),R=p+x*n,J=R+n/2;t.font="10px system-ui, sans-serif",t.textAlign="right",t.textBaseline="middle",t.fillStyle=x===T?o.text:o.textMuted,t.fillText(w.length>C?`${w.slice(0,C-1)}\u2026`:w,r-4,J),(H.get(x)??[]).forEach(Z=>{const K=Z.xIndex,Y=r+K*n,pt=K===O&&x===T,tt=K===x,z=Z.r;if(z===null){t.fillStyle=o.border,t.globalAlpha=.4,t.fillRect(Y+1,R+1,n-2,n-2),t.globalAlpha=1;return}tt?t.fillStyle=o.bgHover:t.fillStyle=vt(z>=0?et:nt,i,Math.abs(z)),t.fillRect(Y+1,R+1,n-2,n-2),n>=28&&!tt&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Math.abs(z)>.6?o.bg:o.text,t.textAlign="center",t.textBaseline="middle",t.fillText(z.toFixed(2),Y+n/2,R+n/2)),pt&&(t.strokeStyle=o.text,t.lineWidth=2,t.strokeRect(Y+1,R+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=o.bg,t.lineWidth=1;for(let h=0;h<=c;h++){const x=r+h*n;t.beginPath(),t.moveTo(x,p),t.lineTo(x,p+c*n),t.stroke();const w=p+h*n;t.beginPath(),t.moveTo(r,w),t.lineTo(r+c*n,w),t.stroke()}}function Q(o){const i=s.getBoundingClientRect(),u=$.clientWidth,n=y(u),_=(o.clientX-i.left)*(s.width/B/i.width),v=(o.clientY-i.top)*(s.height/B/i.height),M=Math.floor((_-r)/n),t=Math.floor((v-p)/n);return M>=0&&M<c&&t>=0&&t<c?{xi:M,yi:t}:null}return s.addEventListener("mousemove",o=>{const i=Q(o);if(!i){S.style.display="none",s.style.cursor="default";return}const{xi:u,yi:n}=i,_=u===n;s.style.cursor=_||!D?"default":"pointer";const v=e.cells.find(h=>h.xIndex===u&&h.yIndex===n),M=e.tests[u].label,t=e.tests[n].label;_?S.innerHTML=`<strong>${M}</strong>`:v?.r!==null&&v?.r!==void 0?S.innerHTML=`<strong>${f(e.tests[n])}</strong> (#${e.tests[n].testNumber}) vs <strong>${f(e.tests[u])}</strong> (#${e.tests[u].testNumber})<br>r = ${v.r.toFixed(4)}${D?"<br><em>click to view in scatter</em>":""}`:S.innerHTML=`${t} vs ${M}<br><em>insufficient data</em>`,S.style.display="block",bt(S,l,o.clientX,o.clientY)}),s.addEventListener("mouseleave",()=>{S.style.display="none"}),s.addEventListener("click",o=>{const i=Q(o);if(!i||!D)return;const{xi:u,yi:n}=i;u!==n&&(O=u,T=n,X(),D(e.tests[u].testNumber,e.tests[n].testNumber))}),X}function G(){const e=rt(),s=e.flatMap(b=>b.dies??[]),c=ut(s,k),{matrix:f,strongPairs:C,moderatePairs:r,hiddenWeakPairs:E,strongestPair:p}=ft(c,{minTests:6,maxTests:F}),W=mt(e,{facetableOnly:!0}).filter(b=>b.splittable).map(b=>b.label);at(C,r,E,p,W),q=ct(f),q()}const dt=xt(l,()=>q());return G(),{card:l,destroy:()=>dt.disconnect()}}
1
+ import{buildCorrelationMatrix as mt,filterCorrelationMatrix as ht}from"../../stats/correlation.js";import{CORRELATION_POSITIVE as st,CORRELATION_NEGATIVE as lt}from"./palette.js";import{buildFacetTable as xt}from"../../stats/facets.js";import{CLR as b}from"../toolbar.js";import{attachChartTip as gt,cardShell as bt,observeResize as yt,makeTooltip as Mt,positionChartTooltip as wt,makeLabeledSelect as Lt,makeWaferSelect as vt,renderEmptyState as Ct,resolveChartCanvasColors as It}from"./chartShell.js";const it=5,rt=100,at=20;function Z(y){const r=y.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(r)return[+r[1],+r[2],+r[3]];const c=y.trim().replace("#","");return c.length===6?[parseInt(c.slice(0,2),16),parseInt(c.slice(2,4),16),parseInt(c.slice(4,6),16)]:null}function Et(y){const r=Z(y);return r?(r[0]*299+r[1]*587+r[2]*114)/1e3>140?"#111":"#fff":"#000"}function Tt(y,r,c){const k=Z(y);if(!k)return y;const P=Math.round(r[0]+(k[0]-r[0])*c),m=Math.round(r[1]+(k[1]-r[1])*c),D=Math.round(r[2]+(k[2]-r[2])*c);return`rgb(${P},${m},${D})`}export function renderCorrelationPanel(y){const{title:r="Test correlation matrix",items:c,testDefs:k,onSaveImage:P,groups:m,onSelectPair:D}=y,{card:l,body:S,controlsRow:V}=bt(r,P,y.ownerDocument);S.style.overflowX="auto",l.style.alignSelf="start";let j=at,U=m&&m.length>0?m[0].key:void 0,N=null;const H=l.ownerDocument.createElement("label");H.textContent="Max tests:",Object.assign(H.style,{color:b.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const v=l.ownerDocument.createElement("input");v.type="number",v.min=String(it),v.max=String(rt),v.value=String(j),Object.assign(v.style,{width:"52px",fontSize:"12px",padding:"2px 4px",background:b.menuBg,color:b.value,border:`1px solid ${b.menuBorder}`,borderRadius:"3px"}),v.addEventListener("change",()=>{const e=Math.max(it,Math.min(rt,parseInt(v.value,10)||at));v.value=String(e),e!==j&&(j=e,X())}),H.appendChild(v),V.appendChild(H),m&&m.length>0?V.appendChild(Lt("Group:",m.map(e=>({value:e.key,label:e.key})),U??"",e=>{U=e,X()},{ownerDocument:l.ownerDocument})):c.length>1&&V.appendChild(vt(c,N,e=>{N=e,X()},{ownerDocument:l.ownerDocument}));function ct(){return m&&m.length>0?m.find(e=>e.key===U)?.items??[]:N!==null?c[N]?[c[N]]:[]:c}const A=l.ownerDocument.createElement("div");Object.assign(A.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),l.insertBefore(A,S);const B=window.devicePixelRatio||1,I=Mt(l);gt(H,l,I,"Cap on how many tests the matrix includes (strongest correlations kept first)");let q=()=>{};function dt(e,s,d,f,E){if(A.innerHTML="",E.length>0){const h=l.ownerDocument.createElement("div");h.textContent=`\u26A0 Mixed ${E.join(", ")} within this set \u2014 correlations may be misleading (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,Object.assign(h.style,{color:b.warnText,background:b.warnBg,border:`1px solid ${b.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px"}),A.appendChild(h)}const a=l.ownerDocument.createElement("span");Object.assign(a.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:b.label,fontSize:"11px",flexWrap:"wrap"});const T=l.ownerDocument.createElement("span");T.textContent="Pearson r \xB7 click a cell to view that pair in scatter \xB7",a.appendChild(T);const p=l.ownerDocument.createElement("span");Object.assign(p.style,{display:"inline-flex",alignItems:"center",gap:"4px"});const _=l.ownerDocument.createElement("span");_.textContent="\u22121";const M=l.ownerDocument.createElement("span");Object.assign(M.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:"2px",border:`1px solid ${b.menuBorder}`,background:`linear-gradient(to right, ${lt}, ${b.menuBg}, ${st})`});const z=l.ownerDocument.createElement("span");z.textContent="+1",p.append(_,M,z),a.appendChild(p),A.appendChild(a);const $=l.ownerDocument.createElement("span");if(Object.assign($.style,{color:b.value,fontSize:"12px",fontWeight:"500"}),e===0&&s===0)$.textContent=f?`No significant correlations found \u2014 strongest pair: ${f.xLabel} \u2194 ${f.yLabel} (r = ${f.r.toFixed(2)})`:"No significant correlations found";else{const h=[];e>0&&h.push(`${e} strong (|r| \u2265 0.7)`),s>0&&h.push(`${s} moderate (0.4\u20130.7)`);const F=e+s;let W=h.join(", ")+` pair${F!==1?"s":""} found`;d>0&&(W+=` \xB7 ${d} weak pair${d!==1?"s":""} not shown`),$.textContent=W}A.appendChild($)}function pt(e){if(S.innerHTML="",e.tests.length<2)return Ct(S,"Need at least two parametric tests for a correlation matrix."),()=>{};const s=l.ownerDocument.createElement("canvas");s.style.display="block",s.style.cursor="default",S.appendChild(s);const d=e.tests.length,f=o=>o.label.split(" (#")[0],E=Math.min(14,Math.max(...e.tests.map(o=>f(o).length))),a=E*6.5+8,T=10,p=Math.round(T*6.5*Math.sin(Math.PI/4))+14,_=14,M=28,z=44;let $=-1,h=-1;function F(o){const i=Math.max(0,o-a);return Math.max(_,Math.min(z,Math.floor(i/d)))}const W=new Map;for(const o of e.cells){let i=W.get(o.yIndex);i||(i=[],W.set(o.yIndex,i)),i.push(o)}function tt(){const o=It(l),i=Z(o.bg)??[255,255,255],u=S.clientWidth,n=F(u),O=n*d,C=p+n*d+4,w=a+O;s.width=Math.max(1,Math.floor(w*B)),s.height=Math.max(1,Math.floor(C*B)),s.style.width=`${w}px`,s.style.height=`${C}px`;const t=s.getContext("2d");t.setTransform(B,0,0,B,0,0),t.clearRect(0,0,w,C),t.font="10px system-ui, sans-serif",t.fillStyle=o.text,e.tests.forEach((x,g)=>{const L=f(x),R=L.length>T?`${L.slice(0,T-1)}\u2026`:L,J=a+g*n+n/2;t.save(),t.translate(J,p-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=g===$?o.text:o.textMuted,t.fillText(R,0,0),t.restore()}),e.tests.forEach((x,g)=>{const L=f(x),R=p+g*n,J=R+n/2;t.font="10px system-ui, sans-serif",t.textAlign="right",t.textBaseline="middle",t.fillStyle=g===h?o.text:o.textMuted,t.fillText(L.length>E?`${L.slice(0,E-1)}\u2026`:L,a-4,J),(W.get(g)??[]).forEach(nt=>{const K=nt.xIndex,G=a+K*n,ft=K===$&&g===h,ot=K===g,Y=nt.r;if(Y===null){t.fillStyle=o.border,t.globalAlpha=.4,t.fillRect(G+1,R+1,n-2,n-2),t.globalAlpha=1;return}let Q=o.bgHover;ot?t.fillStyle=o.bgHover:(Q=Tt(Y>=0?st:lt,i,Math.abs(Y)),t.fillStyle=Q),t.fillRect(G+1,R+1,n-2,n-2),n>=M&&!ot&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Et(Q),t.textAlign="center",t.textBaseline="middle",t.fillText(Y.toFixed(2),G+n/2,R+n/2)),ft&&(t.strokeStyle=o.text,t.lineWidth=2,t.strokeRect(G+1,R+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=o.bg,t.lineWidth=1;for(let x=0;x<=d;x++){const g=a+x*n;t.beginPath(),t.moveTo(g,p),t.lineTo(g,p+d*n),t.stroke();const L=p+x*n;t.beginPath(),t.moveTo(a,L),t.lineTo(a+d*n,L),t.stroke()}}function et(o){const i=s.getBoundingClientRect(),u=S.clientWidth,n=F(u),O=(o.clientX-i.left)*(s.width/B/i.width),C=(o.clientY-i.top)*(s.height/B/i.height),w=Math.floor((O-a)/n),t=Math.floor((C-p)/n);return w>=0&&w<d&&t>=0&&t<d?{xi:w,yi:t}:null}return s.addEventListener("mousemove",o=>{const i=et(o);if(!i){I.style.display="none",s.style.cursor="default";return}const{xi:u,yi:n}=i,O=u===n;s.style.cursor=O||!D?"default":"pointer";const C=e.cells.find(x=>x.xIndex===u&&x.yIndex===n),w=e.tests[u].label,t=e.tests[n].label;O?I.innerHTML=`<strong>${w}</strong>`:C?.r!==null&&C?.r!==void 0?I.innerHTML=`<strong>${f(e.tests[n])}</strong> (#${e.tests[n].testNumber}) vs <strong>${f(e.tests[u])}</strong> (#${e.tests[u].testNumber})<br>r = ${C.r.toFixed(4)}${D?"<br><em>click to view in scatter</em>":""}`:I.innerHTML=`${t} vs ${w}<br><em>insufficient data</em>`,I.style.display="block",wt(I,l,o.clientX,o.clientY)}),s.addEventListener("mouseleave",()=>{I.style.display="none"}),s.addEventListener("click",o=>{const i=et(o);if(!i||!D)return;const{xi:u,yi:n}=i;u!==n&&($=u,h=n,tt(),D(e.tests[u].testNumber,e.tests[n].testNumber))}),tt}function X(){const e=ct(),s=e.flatMap(M=>M.dies??[]),d=mt(s,k),{matrix:f,strongPairs:E,moderatePairs:a,hiddenWeakPairs:T,strongestPair:p}=ht(d,{minTests:6,maxTests:j}),_=xt(e,{facetableOnly:!0}).filter(M=>M.splittable).map(M=>M.label);dt(E,a,T,p,_),q=pt(f),q()}const ut=yt(l,()=>q());return X(),{card:l,destroy:()=>ut.disconnect()}}
@@ -1 +1 @@
1
- import{buildTestHistogramData as Ht,buildTestHistogramSeries as At}from"../../stats/histogram.js";import{CLR as lt}from"../toolbar.js";import{fmt as st}from"../../renderer/fmt.js";import{QUANTITY as Wt,categorical as Ct}from"./palette.js";import{cardShell as Dt,observeResize as xt,makeTooltip as Et,positionChartTooltip as bt,makeTestSelect as Pt,makeWaferSelect as $t,makeToggle as Bt,renderEmptyState as pt,chartFillHeight as yt,applyCanvasFlow as Mt,resolveChartCanvasColors as Tt,makeAxisFormat as wt,PADDING as tt}from"./chartShell.js";const mt=230,vt=36,at=18;function St(l,B,N,z,rt,R,i,f=4){const j=Math.max(1,R/f),F=Math.pow(10,Math.floor(Math.log10(j))),x=j/F,G=(x<=1?1:x<=2?2:x<=5?5:10)*F,P=z-N;l.save(),l.font="10px system-ui, sans-serif",l.textAlign="right",l.textBaseline="middle";for(let I=0;I<=R+1e-9;I+=G){const _=z-I/(R||1)*P;l.strokeStyle=i.grid,l.lineWidth=1,l.beginPath(),l.moveTo(B,_),l.lineTo(B+rt,_),l.stroke(),l.fillStyle=i.text,l.fillText(String(Math.round(I)),B-4,_)}l.strokeStyle=i.axis,l.lineWidth=1,l.beginPath(),l.moveTo(B,N),l.lineTo(B,z),l.stroke(),l.restore()}export function renderHistogramPanel(l){const{title:B="Test value distribution",items:N,testDefs:z,onSaveImage:rt,groups:R}=l,{card:i,body:f,controlsRow:j}=Dt(B,rt,l.ownerDocument),F=z.filter(r=>r.testNumber!==void 0);let x=l.selectedTestNumber??F[0]?.testNumber??null,G=null,P=!1;const I=Pt(F,x,r=>{x=r,U()},{maxWidth:"200px",ownerDocument:i.ownerDocument});j.appendChild(I);const _=$t(N,G,r=>{G=r,U()},{ownerDocument:i.ownerDocument});j.appendChild(_),j.appendChild(Bt("Axis includes limits",P,r=>{P=r,U()},i.ownerDocument));const A=Et(i);let J=null,y=null;function et(r){const L=z.find(K=>K.testNumber===r);return{unit:L?.unit,limitLow:L?.limitLow,limitHigh:L?.limitHigh}}function U(){if(J?.disconnect(),J=null,f.innerHTML="",x===null){pt(f,"No parametric test data available for a histogram.");return}const r=R&&R.length>0?At(R,x,16,P?et(x).limitLow:void 0,P?et(x).limitHigh:void 0):null;if(_.style.display=r?"none":"",r){if(r.series.length===0){pt(f,"No parametric test data available for a histogram.");return}kt(r);return}const L=G!==null?[N[G]]:N,{unit:K,limitLow:nt,limitHigh:b}=et(x),s=Ht(L,x,16,P?nt:void 0,P?b:void 0);if(s.length===0){pt(f,"No parametric test data available for a histogram.");return}const O=Math.max(...s.map(m=>m.count),1),E=i.ownerDocument.createElement("div");Object.assign(E.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),E.textContent=`max ${O} dies/bucket`,f.appendChild(E);const g=i.ownerDocument.createElement("canvas");g.style.display="block",g.style.cursor="default",f.appendChild(g);let w=-1;const p=window.devicePixelRatio||1,$=s[0].rangeLow,X=s[s.length-1].rangeHigh,ot=X-$||1,Z=wt(Math.max(Math.abs($),Math.abs(X)),K);function Y(m){const u=tt+36,e=g.clientWidth-u-tt,n=m-vt-at;return{plotX:u,plotMaxWidth:Math.max(10,e),plotMaxHeight:n,plotTop:at}}function ct(m){const{plotX:u,plotMaxWidth:e}=Y(mt);if(m<u||m>u+e)return-1;const n=Math.floor((m-u)/e*s.length);return n>=0&&n<s.length?n:-1}function H(){Mt(g,E.offsetHeight);const m=f.clientWidth,u=yt(i,f,g,mt);g.width=Math.max(1,Math.floor(m*p)),g.height=Math.max(1,Math.floor(u*p)),g.style.width=`${m}px`,g.style.height=`${u}px`;const e=g.getContext("2d");e.setTransform(p,0,0,p,0,0),e.clearRect(0,0,m,u),e.font="11px system-ui, sans-serif";const n=Tt(i),{plotX:d,plotMaxWidth:t,plotMaxHeight:h,plotTop:o}=Y(u),a=o+h,v=t/s.length;St(e,d,o,a,t,O,{text:n.textMuted,axis:n.border,grid:n.border}),s.forEach((M,C)=>{const S=M.count/O*h,c=d+C*v,k=a-S;e.fillStyle=C===w?n.text:Wt,e.globalAlpha=C===w?1:.7,e.fillRect(c+1,k,Math.max(1,v-2),S),e.globalAlpha=1,C===w&&(e.fillStyle=n.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${M.count}`,c+v/2,k-2))});const V=M=>d+(M-$)/ot*t;e.font="10px system-ui, sans-serif";for(const[M,C]of[[nt,"LSL"],[b,"USL"]]){if(M===void 0)continue;const S=V(M);e.strokeStyle=n.warnBorder,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(S,o),e.lineTo(S,a),e.stroke(),e.setLineDash([]),e.fillStyle=n.warnBorder,e.textAlign="center",e.textBaseline="bottom",e.fillText(C,S,o-2)}e.font="11px system-ui, sans-serif",e.strokeStyle=n.border,e.lineWidth=1,e.beginPath(),e.moveTo(d,a),e.lineTo(d+t,a),e.stroke(),e.fillStyle=n.textMuted,e.textAlign="center",e.textBaseline="top";const W=Math.max(2,Math.floor(t/44)),dt=Math.ceil(s.length/W),ht=Math.max(1,dt);for(let M=0;M<=s.length;M+=ht){const C=M<s.length?s[M].rangeLow:s[s.length-1].rangeHigh,S=d+M*v;e.beginPath(),e.moveTo(S,a),e.lineTo(S,a+4),e.stroke(),e.fillText(Z.tick(C),S,a+6)}Z.unitLabel&&(e.save(),e.font="10px system-ui, sans-serif",e.fillStyle=n.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${Z.unitLabel})`,d+t,a+20),e.restore())}g.addEventListener("mousemove",m=>{const u=g.getBoundingClientRect(),e=ct(m.clientX-u.left);if(e!==w&&(w=e,H()),e>=0){const n=s[e];A.innerHTML=`<strong>${st(n.rangeLow,K,"engineering")} \u2013 ${st(n.rangeHigh,K,"engineering")}</strong><br>${n.count} dies`,A.style.display="block",bt(A,i,m.clientX,m.clientY)}else A.style.display="none"}),g.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,H()),A.style.display="none"}),J=xt(i,()=>H()),H()}function kt(r){const{unit:L,limitLow:K,limitHigh:nt}=et(x),b=r.ranges,s=r.series,O=Ct;y&&!s.some(n=>n.groupKey===y)&&(y=null);const E=Math.max(1,...s.flatMap(n=>n.counts)),g=i.ownerDocument.createElement("div");Object.assign(g.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),g.textContent=`${s.length} groups \xB7 max ${E} dies/bucket`,f.appendChild(g);const w=i.ownerDocument.createElement("div");Object.assign(w.style,{display:"flex",flexWrap:"wrap",gap:"4px 12px",marginBottom:"4px"}),s.forEach((n,d)=>{const t=i.ownerDocument.createElement("button");t.type="button",t.title=`${n.groupKey} \u2014 click to emphasize (dim the rest)`,Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:"5px",fontSize:"11px",padding:"1px 4px",border:"none",background:"none",cursor:"pointer",color:lt.text,borderRadius:"3px"});const h=i.ownerDocument.createElement("span");Object.assign(h.style,{width:"10px",height:"10px",borderRadius:"2px",background:O(d),flex:"0 0 auto"});const o=i.ownerDocument.createElement("span");o.textContent=n.groupKey,t.append(h,o);const a=y!==null&&y!==n.groupKey;t.style.opacity=a?"0.45":"1",y===n.groupKey&&(t.style.background=lt.bgHover),t.setAttribute("aria-pressed",y===n.groupKey?"true":"false"),t.addEventListener("click",()=>{y=y===n.groupKey?null:n.groupKey,U()}),w.appendChild(t)}),f.appendChild(w);const p=i.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",f.appendChild(p);const $=window.devicePixelRatio||1,X=b[0].rangeLow,ot=b[b.length-1].rangeHigh,Z=ot-X||1,Y=wt(Math.max(Math.abs(X),Math.abs(ot)),L),ct=()=>g.offsetHeight+w.offsetHeight;let H=-1,m={plotX:tt+36,plotMaxWidth:10,barWidth:10};function u(){Mt(p,ct());const n=f.clientWidth,d=yt(i,f,p,mt);p.width=Math.max(1,Math.floor(n*$)),p.height=Math.max(1,Math.floor(d*$)),p.style.width=`${n}px`,p.style.height=`${d}px`;const t=p.getContext("2d");t.setTransform($,0,0,$,0,0),t.clearRect(0,0,n,d),t.font="11px system-ui, sans-serif";const h=Tt(i),o=tt+36,a=Math.max(10,n-o-tt),v=at,V=d-vt-at,T=v+V,W=a/b.length;m={plotX:o,plotMaxWidth:a,barWidth:W},St(t,o,v,T,a,E,{text:h.textMuted,axis:h.border,grid:h.border}),H>=0&&(t.fillStyle=h.bgHover,t.globalAlpha=.5,t.fillRect(o+H*W,v,W,V),t.globalAlpha=1);const dt=s.map((c,k)=>({s:c,i:k})).sort((c,k)=>(c.s.groupKey===y?1:0)-(k.s.groupKey===y?1:0));for(const{s:c,i:k}of dt){const D=y===c.groupKey,ut=y!==null&&!D,ft=O(k);t.strokeStyle=ft,t.fillStyle=ft,t.lineWidth=D?2:1,t.globalAlpha=ut?.12:y?.32:.22,t.beginPath(),t.moveTo(o,T),c.counts.forEach((gt,it)=>{const Q=o+it*W,q=T-gt/E*V;t.lineTo(Q,q),t.lineTo(Q+W,q)}),t.lineTo(o+a,T),t.closePath(),t.fill(),t.globalAlpha=ut?.3:1,t.beginPath(),c.counts.forEach((gt,it)=>{const Q=o+it*W,q=T-gt/E*V;it===0?t.moveTo(Q,q):t.lineTo(Q,q),t.lineTo(Q+W,q)}),t.stroke()}t.globalAlpha=1;const ht=c=>o+(c-X)/Z*a;t.font="10px system-ui, sans-serif";for(const[c,k]of[[K,"LSL"],[nt,"USL"]]){if(c===void 0)continue;const D=ht(c);t.strokeStyle=h.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(D,v),t.lineTo(D,T),t.stroke(),t.setLineDash([]),t.fillStyle=h.warnBorder,t.textAlign="center",t.textBaseline="bottom",t.fillText(k,D,v-2)}t.font="11px system-ui, sans-serif",t.strokeStyle=h.border,t.lineWidth=1,t.beginPath(),t.moveTo(o,T),t.lineTo(o+a,T),t.stroke(),t.fillStyle=h.textMuted,t.textAlign="center",t.textBaseline="top";const C=Math.max(2,Math.floor(a/44)),S=Math.max(1,Math.ceil(b.length/C));for(let c=0;c<=b.length;c+=S){const k=c<b.length?b[c].rangeLow:b[b.length-1].rangeHigh,D=o+c*W;t.beginPath(),t.moveTo(D,T),t.lineTo(D,T+4),t.stroke(),t.fillText(Y.tick(k),D,T+6)}Y.unitLabel&&(t.save(),t.font="10px system-ui, sans-serif",t.fillStyle=h.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${Y.unitLabel})`,o+a,T+20),t.restore())}function e(n){const{plotX:d,plotMaxWidth:t,barWidth:h}=m;if(n<d||n>d+t)return-1;const o=Math.floor((n-d)/h);return o>=0&&o<b.length?o:-1}p.addEventListener("mousemove",n=>{const d=p.getBoundingClientRect(),t=e(n.clientX-d.left);if(t!==H&&(H=t,u()),t>=0){const h=b[t],o=s.map((a,v)=>`<span style="display:inline-block;width:9px;height:9px;border-radius:2px;background:${O(v)};margin-right:5px;"></span>${a.groupKey}: ${a.counts[t]}`).join("<br>");A.innerHTML=`<strong>${st(h.rangeLow,L,"engineering")} \u2013 ${st(h.rangeHigh,L,"engineering")}</strong><br>${o}`,A.style.display="block",bt(A,i,n.clientX,n.clientY)}else A.style.display="none"}),p.addEventListener("mouseleave",()=>{H!==-1&&(H=-1,u()),A.style.display="none"}),J=xt(i,()=>u()),u()}U();function Lt(r){!F.some(L=>L.testNumber===r)||r===x||(x=r,I.value=String(r),U())}return{card:i,setTest:Lt,destroy:()=>J?.disconnect()}}
1
+ import{buildTestHistogramData as Ht,buildTestHistogramSeries as At}from"../../stats/histogram.js";import{CLR as lt}from"../toolbar.js";import{fmt as st}from"../../renderer/fmt.js";import{QUANTITY as Ct,categorical as Wt}from"./palette.js";import{cardShell as Dt,observeResize as xt,makeTooltip as Et,attachChartTip as Pt,positionChartTooltip as bt,makeTestSelect as $t,makeWaferSelect as Bt,makeToggle as Rt,renderEmptyState as pt,chartFillHeight as yt,applyCanvasFlow as Mt,resolveChartCanvasColors as Tt,makeAxisFormat as wt,PADDING as tt}from"./chartShell.js";const mt=230,vt=36,at=18;function St(l,B,N,z,rt,R,i,f=4){const j=Math.max(1,R/f),F=Math.pow(10,Math.floor(Math.log10(j))),x=j/F,G=(x<=1?1:x<=2?2:x<=5?5:10)*F,P=z-N;l.save(),l.font="10px system-ui, sans-serif",l.textAlign="right",l.textBaseline="middle";for(let I=0;I<=R+1e-9;I+=G){const _=z-I/(R||1)*P;l.strokeStyle=i.grid,l.lineWidth=1,l.beginPath(),l.moveTo(B,_),l.lineTo(B+rt,_),l.stroke(),l.fillStyle=i.text,l.fillText(String(Math.round(I)),B-4,_)}l.strokeStyle=i.axis,l.lineWidth=1,l.beginPath(),l.moveTo(B,N),l.lineTo(B,z),l.stroke(),l.restore()}export function renderHistogramPanel(l){const{title:B="Test value distribution",items:N,testDefs:z,onSaveImage:rt,groups:R}=l,{card:i,body:f,controlsRow:j}=Dt(B,rt,l.ownerDocument),F=z.filter(r=>r.testNumber!==void 0);let x=l.selectedTestNumber??F[0]?.testNumber??null,G=null,P=!1;const I=$t(F,x,r=>{x=r,U()},{maxWidth:"200px",ownerDocument:i.ownerDocument});j.appendChild(I);const _=Bt(N,G,r=>{G=r,U()},{ownerDocument:i.ownerDocument});j.appendChild(_),j.appendChild(Rt("Axis includes limits",P,r=>{P=r,U()},i.ownerDocument));const L=Et(i);let J=null,y=null;function et(r){const H=z.find(K=>K.testNumber===r);return{unit:H?.unit,limitLow:H?.limitLow,limitHigh:H?.limitHigh}}function U(){if(J?.disconnect(),J=null,f.innerHTML="",x===null){pt(f,"No parametric test data available for a histogram.");return}const r=R&&R.length>0?At(R,x,16,P?et(x).limitLow:void 0,P?et(x).limitHigh:void 0):null;if(_.style.display=r?"none":"",r){if(r.series.length===0){pt(f,"No parametric test data available for a histogram.");return}kt(r);return}const H=G!==null?[N[G]]:N,{unit:K,limitLow:nt,limitHigh:b}=et(x),s=Ht(H,x,16,P?nt:void 0,P?b:void 0);if(s.length===0){pt(f,"No parametric test data available for a histogram.");return}const O=Math.max(...s.map(m=>m.count),1),E=i.ownerDocument.createElement("div");Object.assign(E.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),E.textContent=`max ${O} dies/bucket`,f.appendChild(E);const g=i.ownerDocument.createElement("canvas");g.style.display="block",g.style.cursor="default",f.appendChild(g);let w=-1;const p=window.devicePixelRatio||1,$=s[0].rangeLow,X=s[s.length-1].rangeHigh,ot=X-$||1,Z=wt(Math.max(Math.abs($),Math.abs(X)),K);function Y(m){const u=tt+36,e=g.clientWidth-u-tt,n=m-vt-at;return{plotX:u,plotMaxWidth:Math.max(10,e),plotMaxHeight:n,plotTop:at}}function ct(m){const{plotX:u,plotMaxWidth:e}=Y(mt);if(m<u||m>u+e)return-1;const n=Math.floor((m-u)/e*s.length);return n>=0&&n<s.length?n:-1}function A(){Mt(g,E.offsetHeight);const m=f.clientWidth,u=yt(i,f,g,mt);g.width=Math.max(1,Math.floor(m*p)),g.height=Math.max(1,Math.floor(u*p)),g.style.width=`${m}px`,g.style.height=`${u}px`;const e=g.getContext("2d");e.setTransform(p,0,0,p,0,0),e.clearRect(0,0,m,u),e.font="11px system-ui, sans-serif";const n=Tt(i),{plotX:d,plotMaxWidth:t,plotMaxHeight:h,plotTop:o}=Y(u),a=o+h,v=t/s.length;St(e,d,o,a,t,O,{text:n.textMuted,axis:n.border,grid:n.border}),s.forEach((M,W)=>{const S=M.count/O*h,c=d+W*v,k=a-S;e.fillStyle=W===w?n.text:Ct,e.globalAlpha=W===w?1:.7,e.fillRect(c+1,k,Math.max(1,v-2),S),e.globalAlpha=1,W===w&&(e.fillStyle=n.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${M.count}`,c+v/2,k-2))});const V=M=>d+(M-$)/ot*t;e.font="10px system-ui, sans-serif";for(const[M,W]of[[nt,"LSL"],[b,"USL"]]){if(M===void 0)continue;const S=V(M);e.strokeStyle=n.warnBorder,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(S,o),e.lineTo(S,a),e.stroke(),e.setLineDash([]),e.fillStyle=n.warnBorder,e.textAlign="center",e.textBaseline="bottom",e.fillText(W,S,o-2)}e.font="11px system-ui, sans-serif",e.strokeStyle=n.border,e.lineWidth=1,e.beginPath(),e.moveTo(d,a),e.lineTo(d+t,a),e.stroke(),e.fillStyle=n.textMuted,e.textAlign="center",e.textBaseline="top";const C=Math.max(2,Math.floor(t/44)),dt=Math.ceil(s.length/C),ht=Math.max(1,dt);for(let M=0;M<=s.length;M+=ht){const W=M<s.length?s[M].rangeLow:s[s.length-1].rangeHigh,S=d+M*v;e.beginPath(),e.moveTo(S,a),e.lineTo(S,a+4),e.stroke(),e.fillText(Z.tick(W),S,a+6)}Z.unitLabel&&(e.save(),e.font="10px system-ui, sans-serif",e.fillStyle=n.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${Z.unitLabel})`,d+t,a+20),e.restore())}g.addEventListener("mousemove",m=>{const u=g.getBoundingClientRect(),e=ct(m.clientX-u.left);if(e!==w&&(w=e,A()),e>=0){const n=s[e];L.innerHTML=`<strong>${st(n.rangeLow,K,"engineering")} \u2013 ${st(n.rangeHigh,K,"engineering")}</strong><br>${n.count} dies`,L.style.display="block",bt(L,i,m.clientX,m.clientY)}else L.style.display="none"}),g.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,A()),L.style.display="none"}),J=xt(i,()=>A()),A()}function kt(r){const{unit:H,limitLow:K,limitHigh:nt}=et(x),b=r.ranges,s=r.series,O=Wt;y&&!s.some(n=>n.groupKey===y)&&(y=null);const E=Math.max(1,...s.flatMap(n=>n.counts)),g=i.ownerDocument.createElement("div");Object.assign(g.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),g.textContent=`${s.length} groups \xB7 max ${E} dies/bucket`,f.appendChild(g);const w=i.ownerDocument.createElement("div");Object.assign(w.style,{display:"flex",flexWrap:"wrap",gap:"4px 12px",marginBottom:"4px"}),s.forEach((n,d)=>{const t=i.ownerDocument.createElement("button");t.type="button",Pt(t,i,L,`${n.groupKey} \u2014 click to emphasize (dim the rest)`),Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:"5px",fontSize:"11px",padding:"1px 4px",border:"none",background:"none",cursor:"pointer",color:lt.text,borderRadius:"3px"});const h=i.ownerDocument.createElement("span");Object.assign(h.style,{width:"10px",height:"10px",borderRadius:"2px",background:O(d),flex:"0 0 auto"});const o=i.ownerDocument.createElement("span");o.textContent=n.groupKey,t.append(h,o);const a=y!==null&&y!==n.groupKey;t.style.opacity=a?"0.45":"1",y===n.groupKey&&(t.style.background=lt.bgHover),t.setAttribute("aria-pressed",y===n.groupKey?"true":"false"),t.addEventListener("click",()=>{y=y===n.groupKey?null:n.groupKey,U()}),w.appendChild(t)}),f.appendChild(w);const p=i.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",f.appendChild(p);const $=window.devicePixelRatio||1,X=b[0].rangeLow,ot=b[b.length-1].rangeHigh,Z=ot-X||1,Y=wt(Math.max(Math.abs(X),Math.abs(ot)),H),ct=()=>g.offsetHeight+w.offsetHeight;let A=-1,m={plotX:tt+36,plotMaxWidth:10,barWidth:10};function u(){Mt(p,ct());const n=f.clientWidth,d=yt(i,f,p,mt);p.width=Math.max(1,Math.floor(n*$)),p.height=Math.max(1,Math.floor(d*$)),p.style.width=`${n}px`,p.style.height=`${d}px`;const t=p.getContext("2d");t.setTransform($,0,0,$,0,0),t.clearRect(0,0,n,d),t.font="11px system-ui, sans-serif";const h=Tt(i),o=tt+36,a=Math.max(10,n-o-tt),v=at,V=d-vt-at,T=v+V,C=a/b.length;m={plotX:o,plotMaxWidth:a,barWidth:C},St(t,o,v,T,a,E,{text:h.textMuted,axis:h.border,grid:h.border}),A>=0&&(t.fillStyle=h.bgHover,t.globalAlpha=.5,t.fillRect(o+A*C,v,C,V),t.globalAlpha=1);const dt=s.map((c,k)=>({s:c,i:k})).sort((c,k)=>(c.s.groupKey===y?1:0)-(k.s.groupKey===y?1:0));for(const{s:c,i:k}of dt){const D=y===c.groupKey,ut=y!==null&&!D,ft=O(k);t.strokeStyle=ft,t.fillStyle=ft,t.lineWidth=D?2:1,t.globalAlpha=ut?.12:y?.32:.22,t.beginPath(),t.moveTo(o,T),c.counts.forEach((gt,it)=>{const Q=o+it*C,q=T-gt/E*V;t.lineTo(Q,q),t.lineTo(Q+C,q)}),t.lineTo(o+a,T),t.closePath(),t.fill(),t.globalAlpha=ut?.3:1,t.beginPath(),c.counts.forEach((gt,it)=>{const Q=o+it*C,q=T-gt/E*V;it===0?t.moveTo(Q,q):t.lineTo(Q,q),t.lineTo(Q+C,q)}),t.stroke()}t.globalAlpha=1;const ht=c=>o+(c-X)/Z*a;t.font="10px system-ui, sans-serif";for(const[c,k]of[[K,"LSL"],[nt,"USL"]]){if(c===void 0)continue;const D=ht(c);t.strokeStyle=h.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(D,v),t.lineTo(D,T),t.stroke(),t.setLineDash([]),t.fillStyle=h.warnBorder,t.textAlign="center",t.textBaseline="bottom",t.fillText(k,D,v-2)}t.font="11px system-ui, sans-serif",t.strokeStyle=h.border,t.lineWidth=1,t.beginPath(),t.moveTo(o,T),t.lineTo(o+a,T),t.stroke(),t.fillStyle=h.textMuted,t.textAlign="center",t.textBaseline="top";const W=Math.max(2,Math.floor(a/44)),S=Math.max(1,Math.ceil(b.length/W));for(let c=0;c<=b.length;c+=S){const k=c<b.length?b[c].rangeLow:b[b.length-1].rangeHigh,D=o+c*C;t.beginPath(),t.moveTo(D,T),t.lineTo(D,T+4),t.stroke(),t.fillText(Y.tick(k),D,T+6)}Y.unitLabel&&(t.save(),t.font="10px system-ui, sans-serif",t.fillStyle=h.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${Y.unitLabel})`,o+a,T+20),t.restore())}function e(n){const{plotX:d,plotMaxWidth:t,barWidth:h}=m;if(n<d||n>d+t)return-1;const o=Math.floor((n-d)/h);return o>=0&&o<b.length?o:-1}p.addEventListener("mousemove",n=>{const d=p.getBoundingClientRect(),t=e(n.clientX-d.left);if(t!==A&&(A=t,u()),t>=0){const h=b[t],o=s.map((a,v)=>`<span style="display:inline-block;width:9px;height:9px;border-radius:2px;background:${O(v)};margin-right:5px;"></span>${a.groupKey}: ${a.counts[t]}`).join("<br>");L.innerHTML=`<strong>${st(h.rangeLow,H,"engineering")} \u2013 ${st(h.rangeHigh,H,"engineering")}</strong><br>${o}`,L.style.display="block",bt(L,i,n.clientX,n.clientY)}else L.style.display="none"}),p.addEventListener("mouseleave",()=>{A!==-1&&(A=-1,u()),L.style.display="none"}),J=xt(i,()=>u()),u()}U();function Lt(r){!F.some(H=>H.testNumber===r)||r===x||(x=r,I.value=String(r),U())}return{card:i,setTest:Lt,destroy:()=>J?.disconnect()}}
@@ -1 +1 @@
1
- import{getColorScheme as he}from"../../renderer/colorSchemes.js";import{categorical as ge}from"./palette.js";import{buildScatterData as be,buildScatterDataGrouped as ye}from"../../stats/scatter.js";import{buildFacetTable as we}from"../../stats/facets.js";import{CLR as M}from"../toolbar.js";import{cardShell as Se,observeResize as Me,makeTooltip as Te,makeTestSelect as ke,makeWaferSelect as ve,chartFillHeight as Le,applyCanvasFlow as Ce,drawAxisUnit as Ae,resolveChartCanvasColors as De,makeAxisFormat as ne}from"./chartShell.js";const c=52,Be=16,a=16,Oe=44;export function renderScatterPanel(W){const{title:oe="Test scatter",items:D,testDefs:_,colorScheme:ie="default",onSaveImage:le,groups:P}=W,{card:i,body:C,controlsRow:X}=Se(oe,le,W.ownerDocument),R=_.filter(t=>t.testNumber!==void 0),y=!!(P&&P.length>0),Y=(P??[]).map(t=>t.key),ae=new Map(Y.map((t,n)=>[t,n]));let x=W.xTestNumber??R[0]?.testNumber??null,f=W.yTestNumber??R[1]?.testNumber??x,B=null;const q=()=>B!==null?D[B]?[D[B]]:[]:D;function K(t,n,o){const l=i.ownerDocument.createElement("label");Object.assign(l.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:M.label});const p=i.ownerDocument.createElement("span");p.textContent=t;const m=ke(R,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:i.ownerDocument});return l.append(p,m),{wrap:l,select:m}}const{wrap:se,select:re}=K("X:",x,t=>{x=t,E()}),{wrap:ce,select:de}=K("Y:",f,t=>{f=t,E()});X.append(se,ce),!y&&D.length>1&&X.appendChild(ve(D,B,t=>{B=t,E()},{ownerDocument:i.ownerDocument}));const O=i.ownerDocument.createElement("div");Object.assign(O.style,{color:M.warnText,background:M.warnBg,border:`1px solid ${M.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",marginBottom:"4px",display:"none"}),i.insertBefore(O,C);function J(){const t=y?[]:we(q(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);t.length>0?(O.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.`,O.style.display=""):O.style.display="none"}J();const j=i.ownerDocument.createElement("div");j.textContent=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",Object.assign(j.style,{color:M.label,fontSize:"11px",marginBottom:"4px"}),i.insertBefore(j,C);const pe=he(ie),{forBin:me}=pe,G=t=>y?t.group??"\u2014":String(t.hbin??0),Q=t=>y?ge(ae.get(t)??0):me(Number(t)),V=t=>y?t:t==="0"?"No bin data":`Bin ${t}`,b=new Set,A=i.ownerDocument.createElement("div");Object.assign(A.style,{display:"flex",flexWrap:"wrap",gap:"4px",marginBottom:"6px"}),C.appendChild(A);const h=i.ownerDocument.createElement("canvas");h.style.display="block",h.style.cursor="crosshair",C.appendChild(h);const F=window.devicePixelRatio||1,Ee=Te(i);let g=[],T=0,I=1,k=0,N=1;function $(t){const n=_.find(o=>o.testNumber===t);return{unit:n?.unit,limitLow:n?.limitLow,limitHigh:n?.limitHigh}}function xe(){const t=C.clientWidth,n=Math.max(200,Math.min(400,t*.65)),o=Le(i,C,h,n);return{w:t,h:o,plotW:Math.max(10,t-c-Be),plotH:Math.max(10,o-a-Oe)}}function Z(){for(const t of A.querySelectorAll("[data-cat]")){const n=t.dataset.cat,o=b.size===0||b.has(n);t.style.opacity=o?"1":"0.35";const l=b.has(n);t.style.boxShadow=l?`0 0 0 2px ${M.text}`:"none",t.setAttribute("aria-pressed",l?"true":"false")}}function ee(t){A.innerHTML="",b.clear();for(const n of t){const o=i.ownerDocument.createElement("button");o.type="button",o.dataset.cat=n,o.title=`${V(n)} \u2014 click to filter`;const l=Q(n);Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:"4px",padding:"2px 7px",borderRadius:"10px",border:`1px solid ${M.menuBorder}`,background:"none",cursor:"pointer",fontSize:"11px",color:M.text,whiteSpace:"nowrap"});const p=i.ownerDocument.createElement("span");Object.assign(p.style,{display:"inline-block",width:"9px",height:"9px",borderRadius:"50%",background:l,flexShrink:"0"}),o.append(p,i.ownerDocument.createTextNode(V(n))),o.addEventListener("click",()=>{b.has(n)?b.delete(n):b.add(n),Z(),z()}),A.appendChild(o)}Z()}function z(){Ce(h,A.offsetHeight);const t=De(i),n=I-T,o=N-k,{w:l,h:p,plotW:m,plotH:d}=xe();h.width=Math.max(1,Math.floor(l*F)),h.height=Math.max(1,Math.floor(p*F)),h.style.width=`${l}px`,h.style.height=`${p}px`;const e=h.getContext("2d");if(e.setTransform(F,0,0,F,0,0),e.clearRect(0,0,l,p),g.length===0){e.font="12px 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.",l/2,p/2);return}const w=4,v=ne(Math.max(Math.abs(T),Math.abs(I)),x!==null?$(x).unit:void 0),U=ne(Math.max(Math.abs(k),Math.abs(N)),f!==null?$(f).unit:void 0);e.font="10px system-ui, sans-serif",e.strokeStyle=t.border,e.lineWidth=.5,e.fillStyle=t.textMuted;for(let s=0;s<=w;s++){const S=T+n*s/w,r=c+s/w*m;e.beginPath(),e.moveTo(r,a),e.lineTo(r,a+d),e.stroke(),e.textAlign="center",e.textBaseline="top",e.fillText(v.tick(S),r,a+d+4);const L=k+o*s/w,u=a+(1-s/w)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="right",e.textBaseline="middle",e.fillText(U.tick(L),c-4,u)}v.unitLabel&&Ae(e,v.unitLabel,c+m/2,a+d+24,t.textMuted),U.unitLabel&&(e.save(),e.font="10px system-ui, sans-serif",e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.translate(6,a+d/2),e.rotate(-Math.PI/2),e.fillText(`(${U.unitLabel})`,0,0),e.restore());const H=b.size===0?g:g.filter(s=>b.has(G(s))),te=H.length>5e3?Math.ceil(H.length/5e3):1;e.globalAlpha=Math.max(.15,Math.min(.7,200/(H.length/te)));for(let s=0;s<H.length;s+=te){const S=H[s],r=c+(S.x-T)/n*m,L=a+(1-(S.y-k)/o)*d;e.beginPath(),e.arc(r,L,2.5,0,Math.PI*2),e.fillStyle=Q(G(S)),e.fill()}if(e.globalAlpha=1,x!==null&&f!==null){const s=$(x),S=$(f);e.strokeStyle=t.textMuted,e.fillStyle=t.textMuted,e.lineWidth=1,e.setLineDash([4,3]),e.font="9px system-ui, sans-serif",e.textBaseline="top";for(const[r,L]of[[s.limitLow,"LSL"],[s.limitHigh,"USL"]]){if(r===void 0||r<T||r>I)continue;const u=c+(r-T)/n*m;e.beginPath(),e.moveTo(u,a),e.lineTo(u,a+d),e.stroke(),e.textAlign="left",e.fillText(L,u+2,a+2)}for(const[r,L]of[[S.limitLow,"LSL"],[S.limitHigh,"USL"]]){if(r===void 0||r<k||r>N)continue;const u=a+(1-(r-k)/o)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="left",e.fillText(L,c+3,u+2)}e.setLineDash([])}e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,a),e.lineTo(c,a+d),e.moveTo(c,a+d),e.lineTo(c+m,a+d),e.stroke()}function E(){if(J(),R.length<2||x===null||f===null){g=[],ee([]),z();return}g=y?ye(P,x,f):be(q(),x,f);let t;if(y){const n=new Set(g.map(G));t=Y.filter(o=>n.has(o))}else t=Array.from(new Set(g.map(n=>n.hbin??0))).sort((n,o)=>n-o).map(String);if(ee(t),g.length>0){const n=g.map(v=>v.x),o=g.map(v=>v.y),l=Math.min(...n),p=Math.max(...n),m=Math.min(...o),d=Math.max(...o),e=(p-l)*.05||1,w=(d-m)*.05||1;T=l-e,I=p+e,k=m-w,N=d+w}z()}const ue=Me(i,()=>z());E();function fe(t,n){x=t,f=n,re.value=String(t),de.value=String(n),E()}return{card:i,setXY:fe,destroy:()=>ue.disconnect()}}
1
+ import{getColorScheme as ge}from"../../renderer/colorSchemes.js";import{categorical as be}from"./palette.js";import{buildScatterData as ye,buildScatterDataGrouped as we}from"../../stats/scatter.js";import{buildFacetTable as Se}from"../../stats/facets.js";import{CLR as T}from"../toolbar.js";import{cardShell as Te,observeResize as Me,makeTooltip as ke,attachChartTip as ve,makeTestSelect as Le,makeWaferSelect as Ce,chartFillHeight as Ae,applyCanvasFlow as De,drawAxisUnit as Be,resolveChartCanvasColors as Oe,makeAxisFormat as ne}from"./chartShell.js";const c=52,Ee=16,a=16,He=44;export function renderScatterPanel(W){const{title:oe="Test scatter",items:D,testDefs:_,colorScheme:ie="default",onSaveImage:le,groups:P}=W,{card:i,body:C,controlsRow:X}=Te(oe,le,W.ownerDocument),R=_.filter(t=>t.testNumber!==void 0),y=!!(P&&P.length>0),Y=(P??[]).map(t=>t.key),ae=new Map(Y.map((t,n)=>[t,n]));let x=W.xTestNumber??R[0]?.testNumber??null,f=W.yTestNumber??R[1]?.testNumber??x,B=null;const q=()=>B!==null?D[B]?[D[B]]:[]:D;function K(t,n,o){const l=i.ownerDocument.createElement("label");Object.assign(l.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:T.label});const p=i.ownerDocument.createElement("span");p.textContent=t;const m=Le(R,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:i.ownerDocument});return l.append(p,m),{wrap:l,select:m}}const{wrap:se,select:re}=K("X:",x,t=>{x=t,E()}),{wrap:ce,select:de}=K("Y:",f,t=>{f=t,E()});X.append(se,ce),!y&&D.length>1&&X.appendChild(Ce(D,B,t=>{B=t,E()},{ownerDocument:i.ownerDocument}));const O=i.ownerDocument.createElement("div");Object.assign(O.style,{color:T.warnText,background:T.warnBg,border:`1px solid ${T.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",marginBottom:"4px",display:"none"}),i.insertBefore(O,C);function J(){const t=y?[]:Se(q(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);t.length>0?(O.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.`,O.style.display=""):O.style.display="none"}J();const j=i.ownerDocument.createElement("div");j.textContent=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",Object.assign(j.style,{color:T.label,fontSize:"11px",marginBottom:"4px"}),i.insertBefore(j,C);const pe=ge(ie),{forBin:me}=pe,G=t=>y?t.group??"\u2014":String(t.hbin??0),Q=t=>y?be(ae.get(t)??0):me(Number(t)),V=t=>y?t:t==="0"?"No bin data":`Bin ${t}`,b=new Set,A=i.ownerDocument.createElement("div");Object.assign(A.style,{display:"flex",flexWrap:"wrap",gap:"4px",marginBottom:"6px"}),C.appendChild(A);const h=i.ownerDocument.createElement("canvas");h.style.display="block",h.style.cursor="crosshair",C.appendChild(h);const F=window.devicePixelRatio||1,xe=ke(i);let g=[],M=0,I=1,k=0,N=1;function $(t){const n=_.find(o=>o.testNumber===t);return{unit:n?.unit,limitLow:n?.limitLow,limitHigh:n?.limitHigh}}function ue(){const t=C.clientWidth,n=Math.max(200,Math.min(400,t*.65)),o=Ae(i,C,h,n);return{w:t,h:o,plotW:Math.max(10,t-c-Ee),plotH:Math.max(10,o-a-He)}}function Z(){for(const t of A.querySelectorAll("[data-cat]")){const n=t.dataset.cat,o=b.size===0||b.has(n);t.style.opacity=o?"1":"0.35";const l=b.has(n);t.style.boxShadow=l?`0 0 0 2px ${T.text}`:"none",t.setAttribute("aria-pressed",l?"true":"false")}}function ee(t){A.innerHTML="",b.clear();for(const n of t){const o=i.ownerDocument.createElement("button");o.type="button",o.dataset.cat=n,ve(o,i,xe,`${V(n)} \u2014 click to filter`);const l=Q(n);Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:"4px",padding:"2px 7px",borderRadius:"10px",border:`1px solid ${T.menuBorder}`,background:"none",cursor:"pointer",fontSize:"11px",color:T.text,whiteSpace:"nowrap"});const p=i.ownerDocument.createElement("span");Object.assign(p.style,{display:"inline-block",width:"9px",height:"9px",borderRadius:"50%",background:l,flexShrink:"0"}),o.append(p,i.ownerDocument.createTextNode(V(n))),o.addEventListener("click",()=>{b.has(n)?b.delete(n):b.add(n),Z(),z()}),A.appendChild(o)}Z()}function z(){De(h,A.offsetHeight);const t=Oe(i),n=I-M,o=N-k,{w:l,h:p,plotW:m,plotH:d}=ue();h.width=Math.max(1,Math.floor(l*F)),h.height=Math.max(1,Math.floor(p*F)),h.style.width=`${l}px`,h.style.height=`${p}px`;const e=h.getContext("2d");if(e.setTransform(F,0,0,F,0,0),e.clearRect(0,0,l,p),g.length===0){e.font="12px 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.",l/2,p/2);return}const w=4,v=ne(Math.max(Math.abs(M),Math.abs(I)),x!==null?$(x).unit:void 0),U=ne(Math.max(Math.abs(k),Math.abs(N)),f!==null?$(f).unit:void 0);e.font="10px system-ui, sans-serif",e.strokeStyle=t.border,e.lineWidth=.5,e.fillStyle=t.textMuted;for(let s=0;s<=w;s++){const S=M+n*s/w,r=c+s/w*m;e.beginPath(),e.moveTo(r,a),e.lineTo(r,a+d),e.stroke(),e.textAlign="center",e.textBaseline="top",e.fillText(v.tick(S),r,a+d+4);const L=k+o*s/w,u=a+(1-s/w)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="right",e.textBaseline="middle",e.fillText(U.tick(L),c-4,u)}v.unitLabel&&Be(e,v.unitLabel,c+m/2,a+d+24,t.textMuted),U.unitLabel&&(e.save(),e.font="10px system-ui, sans-serif",e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.translate(6,a+d/2),e.rotate(-Math.PI/2),e.fillText(`(${U.unitLabel})`,0,0),e.restore());const H=b.size===0?g:g.filter(s=>b.has(G(s))),te=H.length>5e3?Math.ceil(H.length/5e3):1;e.globalAlpha=Math.max(.15,Math.min(.7,200/(H.length/te)));for(let s=0;s<H.length;s+=te){const S=H[s],r=c+(S.x-M)/n*m,L=a+(1-(S.y-k)/o)*d;e.beginPath(),e.arc(r,L,2.5,0,Math.PI*2),e.fillStyle=Q(G(S)),e.fill()}if(e.globalAlpha=1,x!==null&&f!==null){const s=$(x),S=$(f);e.strokeStyle=t.textMuted,e.fillStyle=t.textMuted,e.lineWidth=1,e.setLineDash([4,3]),e.font="9px system-ui, sans-serif",e.textBaseline="top";for(const[r,L]of[[s.limitLow,"LSL"],[s.limitHigh,"USL"]]){if(r===void 0||r<M||r>I)continue;const u=c+(r-M)/n*m;e.beginPath(),e.moveTo(u,a),e.lineTo(u,a+d),e.stroke(),e.textAlign="left",e.fillText(L,u+2,a+2)}for(const[r,L]of[[S.limitLow,"LSL"],[S.limitHigh,"USL"]]){if(r===void 0||r<k||r>N)continue;const u=a+(1-(r-k)/o)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="left",e.fillText(L,c+3,u+2)}e.setLineDash([])}e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,a),e.lineTo(c,a+d),e.moveTo(c,a+d),e.lineTo(c+m,a+d),e.stroke()}function E(){if(J(),R.length<2||x===null||f===null){g=[],ee([]),z();return}g=y?we(P,x,f):ye(q(),x,f);let t;if(y){const n=new Set(g.map(G));t=Y.filter(o=>n.has(o))}else t=Array.from(new Set(g.map(n=>n.hbin??0))).sort((n,o)=>n-o).map(String);if(ee(t),g.length>0){const n=g.map(v=>v.x),o=g.map(v=>v.y),l=Math.min(...n),p=Math.max(...n),m=Math.min(...o),d=Math.max(...o),e=(p-l)*.05||1,w=(d-m)*.05||1;M=l-e,I=p+e,k=m-w,N=d+w}z()}const fe=Me(i,()=>z());E();function he(t,n){x=t,f=n,re.value=String(t),de.value=String(n),E()}return{card:i,setXY:he,destroy:()=>fe.disconnect()}}
@@ -42,6 +42,19 @@ export interface GalleryOptions {
42
42
  onViewOptionsChange?: (opts: WaferViewOptions, changed: (keyof WaferViewOptions)[], category: 'preference' | 'state' | 'mixed') => void;
43
43
  /** Legend position for bin modes. Default 'default'. */
44
44
  legendPosition?: 'default' | 'compact' | 'bottom' | 'top' | 'left' | 'floating';
45
+ /**
46
+ * Draw a legend on every card as well as the lot-level one. Default false.
47
+ *
48
+ * The lot legend already names every bin and is interactive, so per-card
49
+ * legends are off by default and the cards spend that width on the wafer.
50
+ * Turn this on when per-card bin highlighting matters more than card size.
51
+ *
52
+ * Ignored — cards always keep their own — when the plot mode is `value` with
53
+ * `colorbarRangeMode: 'data'`, where each card is scaled to its own range and
54
+ * a shared legend could not describe it. The toolbar shows the toggle
55
+ * disabled with that reason rather than appearing to do nothing.
56
+ */
57
+ perCardLegend?: boolean;
45
58
  /** Padding inside each card canvas in CSS pixels. Default 6. */
46
59
  cardPadding?: number;
47
60
  /** Filename stem for the composite gallery PNG. Default 'wafer-gallery'. */
@@ -81,7 +94,7 @@ export interface GalleryOptions {
81
94
  * Options for the always-available Summary panel alongside the gallery
82
95
  * grid — a "Lot" tab with full lot-level stats (metadata, yield, bin
83
96
  * breakdown, ring/quadrant yield, test values, findings) and a combined
84
- * Report button, plus a "Wafers" tab listing every wafer that has its own
97
+ * Report button, plus a "Findings" tab listing every wafer that has its own
85
98
  * per-wafer findings (click a row to open that wafer). Independent of
86
99
  * `insights` below: this always shows/hides its own toolbar button
87
100
  * regardless of whether Insights is open, since Insights has no per-wafer