@wafertools/wafermap 0.24.1 → 0.24.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +5 -0
  3. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  4. package/dist/packages/canvas-adapter/charts/binCluster.d.ts +5 -0
  5. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +5 -0
  7. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/capability.d.ts +5 -0
  9. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +8 -5
  11. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/correlation.d.ts +5 -0
  13. package/dist/packages/canvas-adapter/charts/correlation.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/histogram.d.ts +5 -0
  15. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  16. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +5 -0
  17. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  18. package/dist/packages/canvas-adapter/charts/scatter.d.ts +5 -0
  19. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  20. package/dist/packages/canvas-adapter/dieList.d.ts +9 -0
  21. package/dist/packages/canvas-adapter/dieList.js +6 -6
  22. package/dist/packages/canvas-adapter/insightsTab.d.ts +5 -0
  23. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  24. package/dist/packages/canvas-adapter/metadataBadge.d.ts +4 -0
  25. package/dist/packages/canvas-adapter/metadataBadge.js +1 -1
  26. package/dist/packages/canvas-adapter/renderWaferGallery.js +6 -6
  27. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  28. package/dist/packages/canvas-adapter/summaryPanel.d.ts +1 -1
  29. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  30. package/dist/packages/canvas-adapter/toolbar.d.ts +4 -2
  31. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  32. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  33. package/dist/packages/canvas-adapter/userGuideHtml.js +1 -1
  34. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  35. package/dist/packages/canvas-adapter/version.js +1 -1
  36. package/dist/packages/canvas-adapter/warnings.d.ts +1 -1
  37. package/dist/packages/canvas-adapter/warnings.js +1 -1
  38. package/package.json +5 -5
package/CHANGELOG.md CHANGED
@@ -22,6 +22,48 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
25
+ ## [0.24.3] — 2026-08-25
26
+
27
+ ### Fixed
28
+
29
+ - **The die-list modal's font rendered at browser-default size (larger, unstyled) when opened from a wafer detached into its own popup window**, instead of the intended `11px`. `dieList.ts` injected its `.wmap-dielist-table` stylesheet into the bare global `document` and built every table element the same way — correct for the modal's in-page case (same document as the stylesheet), but wrong once the modal's own anchor-based root resolution correctly placed the box inside a *different* document (the popup's), which never received the `<style>` tag. Fixed by threading an `ownerDocument` through `DieListOptions`/`buildDieListSection` (a new optional field, additive) and `openDieListModal`'s `openModal` call, and switching the module's one-shot `stylesInjected` boolean to a `WeakSet<Document>` keyed per document — a single global flag would have left every *other* document's `<head>` permanently unstyled once the first one injected the rule.
30
+ - **The die-list modal's heading and table sat flush against the box edge**, with no gutter on any side. `openOverlay`'s `contentWrap` carries no padding by design (some callers, e.g. the reparented expand modal, want edge-to-edge content) — the other two `buildDieListSection` call sites each sit inside a parent that already pads its own content, but the modal path never did. `openDieListModal` now sets `contentWrap.style.padding` explicitly.
31
+ - **The toolbar's "Data warnings" popup could render off-screen or clipped when opened from a wafer detached into its own (typically narrower) popup window.** `buildWarningsMenuEl` took an `ownerWindow` parameter for its own right-edge-avoidance math, but neither call site (`renderWaferMap.ts`, `renderWaferGallery.ts`) actually passed it, so the menu was always positioned against the *host page's* `window.innerWidth` regardless of which window it was actually rendering in. Both call sites now pass the button's own `ownerDocument.defaultView`. `buildWarningsBanner` (the Summary panel's warnings block) gained the same doc-aware fix as a matching drift item, even though it had no visible symptom (the DOM adopts nodes created in the wrong document without erroring, which is why this class of bug is silent rather than a crash).
32
+ - **The in-app user guide could open with its live demo widgets non-functional, or its `window.open()` popup could be flagged by the wrong window's popup-blocking heuristics**, when the triggering help button lived on a render inside a detached popup window. `openUserGuideWindow` called the bare global `window.open()`, and its in-page fallback (`openGuideInFloatingWindow`) built the guide's content via the bare global `document`/`window` regardless of the anchor's actual document — both now derive from `anchor.ownerDocument`/its `defaultView`.
33
+ - **`docs/guide.md`'s link to API §5.4.4 was dead in the built docs site.** The heading `#### 5.4.4 Die list & CSV export` was hand-linked using GitHub's anchor-slug convention (`#544-die-list--csv-export`, preserving the double hyphen `&` leaves behind), but Zensical collapses repeated separators when generating heading ids (`#544-die-list-csv-export`, single hyphen) — a different convention from GitHub's, not a Zensical bug. `npm run build:site` reports this class of mismatch only as a non-fatal build warning, so it shipped silently. Every other internal anchor link across `docs/*.md` was checked against the actual built ids and found correct — this was an isolated case, not a pattern.
34
+
35
+ ### Added
36
+
37
+ - `DieListOptions.ownerDocument` (optional) — the document `buildDieListSection` builds its table and injects its stylesheet into. Only relevant for a host mounting the table into a different document than the bare global (e.g. inside a popup window); every existing call is unaffected.
38
+ - Two new themeable colour tokens, `CLR.barFill`/`CLR.barFillMuted` (`--wmap-bar-fill`/`--wmap-bar-fill-muted`), replacing two colours the Summary panel's yield/ring/quadrant progress bars had hardcoded directly instead of exposing to host theming. Documented in `docs/api.md` §5.4.1's token reference table alongside the rest.
39
+
40
+ ### Changed
41
+
42
+ - The Summary panel's own corner radius is now `6px`, matching `UI_STANDARDS.md`'s documented radius for "larger panel-level surfaces" (it was `8px`, an unexplained one-off).
43
+
44
+ ### Internal
45
+
46
+ - `UI_STANDARDS.md` gained three new conventions, prompted by the bugs above and by two z-stacking bugs fixed the same day in v0.24.1 (die-list modal opening behind a host's own `<dialog>`) — all four shared the same shape: a parameter typed optional enough that omitting it by accident compiles cleanly and ships. Documented: every `openModal`/`openFloatingWindow` call site must pass `anchor`; modal/overlay content must set its own `contentWrap` padding (there is no default); and any content that might render inside a detached popup window must thread an `ownerDocument`/`doc` parameter through element creation and `<style>` injection rather than using the bare global.
47
+ - Every element-creation site across `renderWaferMap.ts`, `renderWaferGallery.ts`, `insightsTab.ts`, `metadataBadge.ts`, and all nine `charts/*.ts` panels (barPanel, binCluster, boxplot, capability, correlation, histogram, scatter, regionYieldDiagram, and the shared `chartShell.ts` primitives they're all built from — `cardShell`, `makeSegmented`, `makeBackButton`, `makeToggle`, `makeLabeledSelect`, `makeWaferSelect`, `makeChartGridWrap`, `makeTooltip`, `renderEmptyState`, `saveCanvasPng`) now threads an `ownerDocument` down from the render's own container instead of the bare global `document` — closing the cross-document gap completely, not just at the shared toolbar primitives (`buildCheckMenuEl`, `makeDropdown`, `makeSearchableTestCombo`/`makeTestSelect`, `makeMenuSearchBox`, `summaryPanel.ts`'s internal `el()`/`createSummaryPanelEl`), which were the first pass. Also covers the one non-element-creation instance of the same bug class: `wireMenuKeyboard`'s roving-focus arrow-key handling read the bare global `document.activeElement`, which would have silently broken keyboard navigation in every correctly-doc-aware menu this pass fixed, in the one context (a detached popup) it was fixing them for. All new parameters are optional and default to the bare global, so this is additive, not breaking.
48
+ - New `scripts/check-overlay-conventions.mjs`, wired into `npm run check` — fails the build if a new `openModal`/`openFloatingWindow` call site omits `anchor`, or a new `<style>`-injection site hardcodes `document.head.appendChild` instead of a threaded document. Both conventions above are now enforced, not only documented.
49
+
50
+ ## [0.24.2] — 2026-08-24
51
+
52
+ ### Fixed
53
+
54
+ - **The die-list modal had no reachable vertical scrollbar when the table was wide.** The
55
+ section and its scroll container set `min-height: 0` but not `min-width: 0`. A flex item's
56
+ default `min-width: auto` refuses to shrink below its content's intrinsic minimum, and every
57
+ cell in this table is `white-space: nowrap` — so a wafer carrying many long test-name columns
58
+ (e.g. "Continuity check for TESTMODE pin") stretched the whole section far past its modal.
59
+ The overflow was then clipped by the modal's own `overflow: hidden`, dragging the scroll
60
+ container's vertical scrollbar off the right-hand edge where it could not be seen or used.
61
+ The table looked unscrollable, showing only a stray horizontal scrollbar, with the
62
+ "Export CSV" button and the start of the heading pushed out of view. Both containers now set
63
+ `min-width: 0` alongside `min-height: 0`, so the section stays modal-width and both
64
+ scrollbars work. A narrow table was unaffected, which is why this did not show up in the
65
+ library's own examples.
66
+
25
67
  ## [0.24.1] — 2026-08-24
26
68
 
27
69
  ### Fixed
@@ -43,6 +43,11 @@ export interface ChartPanel {
43
43
  * instead, via `drill.onOpenGroup` — this is never called for those.
44
44
  */
45
45
  onOpen?: (datum: ChartDatum) => void;
46
+ /** Document to build this panel's DOM into. Default `document` — pass the
47
+ * host's own `ownerDocument` when the container might live in a
48
+ * different document (e.g. a gallery card detached into its own popup
49
+ * window). */
50
+ ownerDocument?: Document;
46
51
  }
47
52
  export interface BarPanelHandle {
48
53
  card: HTMLElement;
@@ -1 +1 @@
1
- import{CLR as F}from"../toolbar.js";import{cardShell as U,formatValue as N,observeResize as q,makeTooltip as J,positionChartTooltip as K,makeBackButton as Q,makeSegmented as Z,growCardToFitContent as tt,resolveChartCanvasColors as et,PADDING as m,VALUE_WIDTH as D}from"./chartShell.js";const a=24,p=5,G=110,nt=12;export function renderBarPanel(r,I){const{barColor:O,valueLabel:A}=r;let d=r.title,i=r.data;const{card:h,heading:k,controlsRow:E,body:R}=U(d,I);if(r.selfControl){const t=r.selfControl;E.appendChild(Z(t.options,t.current,n=>X(n)))}const{drill:s}=r;let b=!1,v=null;const $=document.createElement("div");Object.assign($.style,{color:F.label,fontSize:"11px",marginBottom:"6px"}),h.insertBefore($,R);function B(){const t=[];r.onOpen&&t.push("click to open this wafer"),s&&!b&&t.push(`click a ${s.groupLabelText} to see it by wafer`);const n=t.join(", or ");$.textContent=n?`${n[0].toUpperCase()}${n.slice(1)}.`:""}B();const C=document.createElement("div"),L=()=>m*2+Math.min(i.length,nt)*(a+p);Object.assign(C.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${L()}px`,scrollbarGutter:"stable"}),R.appendChild(C);const o=document.createElement("canvas");o.style.display="block",o.style.cursor="default",C.appendChild(o);const y=J(h);let c=-1;const w=window.devicePixelRatio||1;let M=Math.max(1,...i.map(t=>t.value));const S=t=>A?A(t):`${N(t.value)} (${t.percent.toFixed(1)}%)`;function _(t){const n=m+t*(a+p),e=m+G,l=o.clientWidth-e-D-m;return{y:n,barX:e,barMaxWidth:Math.max(10,l)}}function u(){C.style.maxHeight=`${L()}px`,tt(h,R,L());const t=C.clientWidth,n=m*2+i.length*(a+p);o.width=Math.max(1,Math.floor(t*w)),o.height=Math.max(1,Math.floor(n*w)),o.style.width=`${t}px`,o.style.height=`${n}px`;const e=o.getContext("2d");e.setTransform(w,0,0,w,0,0),e.clearRect(0,0,t,n),e.font="11px system-ui, sans-serif",e.textBaseline="middle";const l=et(h);i.forEach((f,g)=>{const{y:x,barX:T,barMaxWidth:H}=_(g);g===c&&(e.fillStyle=l.bgHover,e.fillRect(0,x-p/2,t,a+p)),e.fillStyle=l.text,e.textAlign="right";const j=f.label.length>16?`${f.label.slice(0,15)}\u2026`:f.label;e.fillText(j,m+G-8,x+a/2),e.fillStyle=l.track,e.fillRect(T,x,H,a);const z=Math.max(1,f.value/M*H);e.fillStyle=O?O(f,g):l.text,e.fillRect(T,x,z,a),e.fillStyle=l.text,e.textAlign="right",e.fillText(S(f),T+H+D,x+a/2)})}function X(t){if(!r.selfControl)return;r.selfControl.current=t;const n=r.selfControl.onChange(t);i=n.data,M=Math.max(1,...i.map(e=>e.value)),c=-1,n.title&&(d=n.title,k.textContent=d),u()}function P(t){if(!s)return;const n=s.onOpenGroup(t);i=n.data,d=n.title,k.textContent=d,M=Math.max(1,...i.map(e=>e.value)),c=-1,b||(b=!0,v=Q(()=>V()),E.appendChild(v)),B(),u()}function V(){if(!s)return;const t=s.onBack();i=t.data,d=t.title,k.textContent=d,M=Math.max(1,...i.map(n=>n.value)),c=-1,b=!1,v?.remove(),v=null,B(),u()}function W(t){const n=Math.floor((t-m+p/2)/(a+p));return n>=0&&n<i.length?n:-1}o.addEventListener("mousemove",t=>{const n=o.getBoundingClientRect(),e=W(t.clientY-n.top),l=e>=0&&s&&!b&&i[e].itemCount>1,f=e>=0&&(l||!!r.onOpen&&e>=0&&!l);if(e!==c&&(c=e,o.style.cursor=f?"pointer":"default",u()),e>=0){const g=i[e],x=l?`<br><em>click to see this ${s.groupLabelText} by wafer</em>`:r.onOpen?"<br><em>click to open this wafer</em>":"";y.innerHTML=`<strong>${g.label}</strong><br>${S(g)}${x}`,y.style.display="block",K(y,h,t.clientX,t.clientY)}else y.style.display="none"}),o.addEventListener("mouseleave",()=>{c!==-1&&(c=-1,u()),y.style.display="none"}),o.addEventListener("click",t=>{const n=o.getBoundingClientRect(),e=W(t.clientY-n.top);if(e===-1)return;const l=i[e];if(s&&!b&&l.itemCount>1){P(l);return}r.onOpen?.(l)});const Y=q(h,()=>u());return u(),{card:h,destroy:()=>Y.disconnect()}}
1
+ import{CLR as F}from"../toolbar.js";import{cardShell as U,formatValue as N,observeResize as q,makeTooltip as J,positionChartTooltip as K,makeBackButton as Q,makeSegmented as Z,growCardToFitContent as tt,resolveChartCanvasColors as et,PADDING as m,VALUE_WIDTH as W}from"./chartShell.js";const a=24,p=5,G=110,nt=12;export function renderBarPanel(i,I){const{barColor:H,valueLabel:O}=i;let d=i.title,r=i.data;const{card:c,heading:k,controlsRow:A,body:R}=U(d,I,i.ownerDocument);if(i.selfControl){const t=i.selfControl;A.appendChild(Z(t.options,t.current,n=>X(n),c.ownerDocument))}const{drill:s}=i;let b=!1,v=null;const $=c.ownerDocument.createElement("div");Object.assign($.style,{color:F.label,fontSize:"11px",marginBottom:"6px"}),c.insertBefore($,R);function B(){const t=[];i.onOpen&&t.push("click to open this wafer"),s&&!b&&t.push(`click a ${s.groupLabelText} to see it by wafer`);const n=t.join(", or ");$.textContent=n?`${n[0].toUpperCase()}${n.slice(1)}.`:""}B();const C=c.ownerDocument.createElement("div"),L=()=>m*2+Math.min(r.length,nt)*(a+p);Object.assign(C.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${L()}px`,scrollbarGutter:"stable"}),R.appendChild(C);const o=c.ownerDocument.createElement("canvas");o.style.display="block",o.style.cursor="default",C.appendChild(o);const y=J(c);let f=-1;const w=window.devicePixelRatio||1;let M=Math.max(1,...r.map(t=>t.value));const E=t=>O?O(t):`${N(t.value)} (${t.percent.toFixed(1)}%)`;function _(t){const n=m+t*(a+p),e=m+G,l=o.clientWidth-e-W-m;return{y:n,barX:e,barMaxWidth:Math.max(10,l)}}function u(){C.style.maxHeight=`${L()}px`,tt(c,R,L());const t=C.clientWidth,n=m*2+r.length*(a+p);o.width=Math.max(1,Math.floor(t*w)),o.height=Math.max(1,Math.floor(n*w)),o.style.width=`${t}px`,o.style.height=`${n}px`;const e=o.getContext("2d");e.setTransform(w,0,0,w,0,0),e.clearRect(0,0,t,n),e.font="11px system-ui, sans-serif",e.textBaseline="middle";const l=et(c);r.forEach((h,g)=>{const{y:x,barX:T,barMaxWidth:D}=_(g);g===f&&(e.fillStyle=l.bgHover,e.fillRect(0,x-p/2,t,a+p)),e.fillStyle=l.text,e.textAlign="right";const j=h.label.length>16?`${h.label.slice(0,15)}\u2026`:h.label;e.fillText(j,m+G-8,x+a/2),e.fillStyle=l.track,e.fillRect(T,x,D,a);const z=Math.max(1,h.value/M*D);e.fillStyle=H?H(h,g):l.text,e.fillRect(T,x,z,a),e.fillStyle=l.text,e.textAlign="right",e.fillText(E(h),T+D+W,x+a/2)})}function X(t){if(!i.selfControl)return;i.selfControl.current=t;const n=i.selfControl.onChange(t);r=n.data,M=Math.max(1,...r.map(e=>e.value)),f=-1,n.title&&(d=n.title,k.textContent=d),u()}function P(t){if(!s)return;const n=s.onOpenGroup(t);r=n.data,d=n.title,k.textContent=d,M=Math.max(1,...r.map(e=>e.value)),f=-1,b||(b=!0,v=Q(()=>V(),c.ownerDocument),A.appendChild(v)),B(),u()}function V(){if(!s)return;const t=s.onBack();r=t.data,d=t.title,k.textContent=d,M=Math.max(1,...r.map(n=>n.value)),f=-1,b=!1,v?.remove(),v=null,B(),u()}function S(t){const n=Math.floor((t-m+p/2)/(a+p));return n>=0&&n<r.length?n:-1}o.addEventListener("mousemove",t=>{const n=o.getBoundingClientRect(),e=S(t.clientY-n.top),l=e>=0&&s&&!b&&r[e].itemCount>1,h=e>=0&&(l||!!i.onOpen&&e>=0&&!l);if(e!==f&&(f=e,o.style.cursor=h?"pointer":"default",u()),e>=0){const g=r[e],x=l?`<br><em>click to see this ${s.groupLabelText} by wafer</em>`:i.onOpen?"<br><em>click to open this wafer</em>":"";y.innerHTML=`<strong>${g.label}</strong><br>${E(g)}${x}`,y.style.display="block",K(y,c,t.clientX,t.clientY)}else y.style.display="none"}),o.addEventListener("mouseleave",()=>{f!==-1&&(f=-1,u()),y.style.display="none"}),o.addEventListener("click",t=>{const n=o.getBoundingClientRect(),e=S(t.clientY-n.top);if(e===-1)return;const l=r[e];if(s&&!b&&l.itemCount>1){P(l);return}i.onOpen?.(l)});const Y=q(c,()=>u());return u(),{card:c,destroy:()=>Y.disconnect()}}
@@ -8,6 +8,11 @@ export interface BinClusterPanelOptions {
8
8
  }[];
9
9
  colorScheme?: string;
10
10
  onSaveImage?: SaveImageHandler;
11
+ /** Document to build this panel's DOM into. Default `document` — pass the
12
+ * host's own `ownerDocument` when the container might live in a
13
+ * different document (e.g. a gallery card detached into its own popup
14
+ * window). */
15
+ ownerDocument?: Document;
11
16
  }
12
17
  export interface BinClusterPanelHandle {
13
18
  card: HTMLElement;
@@ -1 +1 @@
1
- import{buildBinClusterData as Y}from"../../stats/binPareto.js";import{categorical as q}from"./palette.js";import{CLR as G}from"../toolbar.js";import{cardShell as J,observeResize as K,makeTooltip as Q,positionChartTooltip as Z,makeSegmented as tt,renderEmptyState as et,growCardToFitContent as nt,resolveChartCanvasColors as ot,PADDING as a,VALUE_WIDTH as P}from"./chartShell.js";const X=90,lt=8,A=1,f=14,it=8;export function renderBinClusterPanel(L){const{groups:j,onSaveImage:D}=L;let x="hbin",M=L.title??"Hard bin pareto";const{card:c,heading:U,body:p,controlsRow:z}=J(M,D);z.appendChild(tt([["hbin","Hard bins"],["sbin","Soft bins"]],x,h=>{x=h,M=`${x==="hbin"?"Hard":"Soft"} bin pareto`,U.textContent=M,I()}));const w=document.createElement("div");w.textContent="One cluster per bin \xB7 a sub-bar per group",Object.assign(w.style,{color:G.label,fontSize:"11px",marginBottom:"6px"}),c.insertBefore(w,p);const b=Q(c);let y=null;function I(){y?.disconnect(),y=null,p.innerHTML="";const h=Y(j,x);if(h.bins.length===0||h.groups.length===0){et(p,"No bin data available for the current grouping.");return}const u=h.groups,g=h.bins,_=q,N=Math.max(1,...g.flatMap(n=>n.counts)),B=u.length*f+(u.length-1)*A,m=B+lt,C=document.createElement("div");Object.assign(C.style,{display:"flex",flexWrap:"wrap",gap:"4px 12px",marginBottom:"4px"}),u.forEach((n,l)=>{const e=document.createElement("span");Object.assign(e.style,{display:"inline-flex",alignItems:"center",gap:"5px",fontSize:"11px",color:G.text});const t=document.createElement("span");Object.assign(t.style,{width:"10px",height:"10px",borderRadius:"2px",background:_(l)});const o=document.createElement("span");o.textContent=n,e.append(t,o),C.appendChild(e)}),p.appendChild(C);const v=document.createElement("div"),$=a*2+Math.min(g.length,it)*m;Object.assign(v.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${$}px`,scrollbarGutter:"stable"}),nt(c,p,C.offsetHeight+$),p.appendChild(v);const i=document.createElement("canvas");i.style.display="block",i.style.cursor="default",v.appendChild(i);const E=window.devicePixelRatio||1;let r=null;function W(){const n=a+X,l=Math.max(10,i.clientWidth-n-P-a);return{barX:n,barMaxWidth:l}}function F(n,l){const e=Math.floor((l-a)/m);if(e<0||e>=g.length)return null;const t=l-a-e*m,o=Math.floor(t/(f+A));if(o<0||o>=u.length)return null;const{barX:s,barMaxWidth:d}=W();return n<s||n>s+d?null:{bin:e,group:o}}function S(){const n=ot(c),l=v.clientWidth,e=a*2+g.length*m;i.width=Math.max(1,Math.floor(l*E)),i.height=Math.max(1,Math.floor(e*E)),i.style.width=`${l}px`,i.style.height=`${e}px`;const t=i.getContext("2d");t.setTransform(E,0,0,E,0,0),t.clearRect(0,0,l,e),t.font="11px system-ui, sans-serif",t.textBaseline="middle";const{barX:o,barMaxWidth:s}=W();g.forEach((d,O)=>{const T=a+O*m;t.fillStyle=n.text,t.textAlign="right",t.fillText(d.label,a+X-8,T+B/2),u.forEach((st,H)=>{const R=T+H*(f+A),k=d.counts[H];r&&r.bin===O&&r.group===H&&(t.fillStyle=n.bgHover,t.fillRect(0,R-1,l,f+2)),t.fillStyle=n.track,t.fillRect(o,R,s,f);const V=Math.max(k>0?1:0,k/N*s);t.fillStyle=_(H),t.fillRect(o,R,V,f)}),t.fillStyle=n.text,t.textAlign="right",t.fillText(`${d.total}`,o+s+P,T+B/2)})}i.addEventListener("mousemove",n=>{const l=i.getBoundingClientRect(),e=F(n.clientX-l.left,n.clientY-l.top),t=e?.bin!==r?.bin||e?.group!==r?.group;if(r=e,t&&S(),e){const o=g[e.bin],s=o.counts[e.group],d=o.total>0?s/o.total*100:0;b.innerHTML=`<strong>${o.label}</strong> \xB7 ${u[e.group]}<br>${s} dies (${d.toFixed(1)}% of bin)`,b.style.display="block",Z(b,c,n.clientX,n.clientY)}else b.style.display="none"}),i.addEventListener("mouseleave",()=>{r&&(r=null,S()),b.style.display="none"}),y=K(c,()=>S()),S()}return I(),{card:c,destroy:()=>y?.disconnect()}}
1
+ import{buildBinClusterData as Y}from"../../stats/binPareto.js";import{categorical as q}from"./palette.js";import{CLR as k}from"../toolbar.js";import{cardShell as J,observeResize as K,makeTooltip as Q,positionChartTooltip as Z,makeSegmented as tt,renderEmptyState as et,growCardToFitContent as nt,resolveChartCanvasColors as ot,PADDING as c,VALUE_WIDTH as G}from"./chartShell.js";const P=90,lt=8,A=1,f=14,it=8;export function renderBinClusterPanel(H){const{groups:X,onSaveImage:j}=H;let x="hbin",M=H.title??"Hard bin pareto";const{card:o,heading:U,body:p,controlsRow:z}=J(M,j,H.ownerDocument);z.appendChild(tt([["hbin","Hard bins"],["sbin","Soft bins"]],x,u=>{x=u,M=`${x==="hbin"?"Hard":"Soft"} bin pareto`,U.textContent=M,L()},o.ownerDocument));const B=o.ownerDocument.createElement("div");B.textContent="One cluster per bin \xB7 a sub-bar per group",Object.assign(B.style,{color:k.label,fontSize:"11px",marginBottom:"6px"}),o.insertBefore(B,p);const m=Q(o);let y=null;function L(){y?.disconnect(),y=null,p.innerHTML="";const u=Y(X,x);if(u.bins.length===0||u.groups.length===0){et(p,"No bin data available for the current grouping.");return}const h=u.groups,g=u.bins,I=q,N=Math.max(1,...g.flatMap(n=>n.counts)),T=h.length*f+(h.length-1)*A,b=T+lt,w=o.ownerDocument.createElement("div");Object.assign(w.style,{display:"flex",flexWrap:"wrap",gap:"4px 12px",marginBottom:"4px"}),h.forEach((n,i)=>{const e=o.ownerDocument.createElement("span");Object.assign(e.style,{display:"inline-flex",alignItems:"center",gap:"5px",fontSize:"11px",color:k.text});const t=o.ownerDocument.createElement("span");Object.assign(t.style,{width:"10px",height:"10px",borderRadius:"2px",background:I(i)});const l=o.ownerDocument.createElement("span");l.textContent=n,e.append(t,l),w.appendChild(e)}),p.appendChild(w);const C=o.ownerDocument.createElement("div"),_=c*2+Math.min(g.length,it)*b;Object.assign(C.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${_}px`,scrollbarGutter:"stable"}),nt(o,p,w.offsetHeight+_),p.appendChild(C);const r=o.ownerDocument.createElement("canvas");r.style.display="block",r.style.cursor="default",C.appendChild(r);const v=window.devicePixelRatio||1;let a=null;function $(){const n=c+P,i=Math.max(10,r.clientWidth-n-G-c);return{barX:n,barMaxWidth:i}}function F(n,i){const e=Math.floor((i-c)/b);if(e<0||e>=g.length)return null;const t=i-c-e*b,l=Math.floor(t/(f+A));if(l<0||l>=h.length)return null;const{barX:s,barMaxWidth:d}=$();return n<s||n>s+d?null:{bin:e,group:l}}function E(){const n=ot(o),i=C.clientWidth,e=c*2+g.length*b;r.width=Math.max(1,Math.floor(i*v)),r.height=Math.max(1,Math.floor(e*v)),r.style.width=`${i}px`,r.style.height=`${e}px`;const t=r.getContext("2d");t.setTransform(v,0,0,v,0,0),t.clearRect(0,0,i,e),t.font="11px system-ui, sans-serif",t.textBaseline="middle";const{barX:l,barMaxWidth:s}=$();g.forEach((d,W)=>{const D=c+W*b;t.fillStyle=n.text,t.textAlign="right",t.fillText(d.label,c+P-8,D+T/2),h.forEach((rt,S)=>{const R=D+S*(f+A),O=d.counts[S];a&&a.bin===W&&a.group===S&&(t.fillStyle=n.bgHover,t.fillRect(0,R-1,i,f+2)),t.fillStyle=n.track,t.fillRect(l,R,s,f);const V=Math.max(O>0?1:0,O/N*s);t.fillStyle=I(S),t.fillRect(l,R,V,f)}),t.fillStyle=n.text,t.textAlign="right",t.fillText(`${d.total}`,l+s+G,D+T/2)})}r.addEventListener("mousemove",n=>{const i=r.getBoundingClientRect(),e=F(n.clientX-i.left,n.clientY-i.top),t=e?.bin!==a?.bin||e?.group!==a?.group;if(a=e,t&&E(),e){const l=g[e.bin],s=l.counts[e.group],d=l.total>0?s/l.total*100:0;m.innerHTML=`<strong>${l.label}</strong> \xB7 ${h[e.group]}<br>${s} dies (${d.toFixed(1)}% of bin)`,m.style.display="block",Z(m,o,n.clientX,n.clientY)}else m.style.display="none"}),r.addEventListener("mouseleave",()=>{a&&(a=null,E()),m.style.display="none"}),y=K(o,()=>E()),E()}return L(),{card:o,destroy:()=>y?.disconnect()}}
@@ -34,6 +34,11 @@ export interface BoxplotPanelOptions {
34
34
  * same test in value mode instead of defaulting to hard-bin mode.
35
35
  * Never called for a pooled group-overview row (that drills instead). */
36
36
  onOpen?: (waferIndex: number, testNumber: number) => void;
37
+ /** Document to build this panel's DOM into. Default `document` — pass the
38
+ * host's own `ownerDocument` when the container might live in a
39
+ * different document (e.g. a gallery card detached into its own popup
40
+ * window). */
41
+ ownerDocument?: Document;
37
42
  }
38
43
  export interface BoxplotPanelHandle {
39
44
  card: HTMLElement;
@@ -1 +1 @@
1
- import{buildTestBoxplotData as It}from"../../stats/boxplot.js";import{CLR as Ht}from"../toolbar.js";import{fmt as gt}from"../../renderer/fmt.js";import{QUANTITY as bt}from"./palette.js";import{cardShell as Rt,observeResize as Wt,makeTooltip as Et,positionChartTooltip as Ot,makeBackButton as _t,makeTestSelect as Dt,makeToggle as yt,renderEmptyState as Tt,growCardToFitContent as Pt,resolveChartCanvasColors as Gt,makeAxisFormat as Xt,PADDING as y,VALUE_WIDTH as Nt}from"./chartShell.js";const T=24,M=5,Mt=110,Yt=12,vt=20;export function renderBoxplotPanel(lt){const{title:N="Test value distribution",items:kt,testDefs:it,onSaveImage:wt,groups:m,groupLabelText:Y="group",onOpen:W}=lt,{card:x,heading:St,body:f,controlsRow:E}=Rt(N,wt);x.style.flex="0 0 auto",f.style.flex="0 0 auto",x.style.alignSelf="start";const O=it.filter(l=>l.testNumber!==void 0);let h=lt.selectedTestNumber??O[0]?.testNumber??null,q=!1,_=!1,d=null,L=null,Q=null,st=null;function Lt(){const l=!m||m.length===0?"":d??"\0overview";if(Q&&st===l)return Q;const n=!m||m.length===0?kt:d!==null?m.find(u=>u.key===d)?.items??[]:m.map(u=>({label:u.key,dies:u.items.flatMap(C=>C.dies??[])}));return Q=n,st=l,n}function U(){d!==null&&!L?(L=_t(()=>{d=null,U(),v()}),E.appendChild(L)):d===null&&L&&(L.remove(),L=null),St.textContent=d!==null?`${N} \u2014 ${Y}: ${d}`:N}const at=Dt(O,h,l=>{h=l,v()},{maxWidth:"240px",emptyText:"No parametric tests"});E.appendChild(at),E.appendChild(yt("Log scale",q,l=>{q=l,v()})),E.appendChild(yt("Axis includes limits",_,l=>{_=l,v()}));const z=document.createElement("div");Object.assign(z.style,{color:Ht.label,fontSize:"11px",marginBottom:"6px"}),x.insertBefore(z,f);function F(){const l=!!m&&m.length>0&&d===null,n=[];l?n.push(`click a ${Y}'s box to see it by wafer`):W&&n.push("click a box to open that wafer");const u=n.length?`${n[0][0].toUpperCase()}${n[0].slice(1)} \xB7 `:"";z.textContent=`${u}box = Q1\u2013Q3, line = median, whiskers = min/max`}F(),U();const $=Et(x);let V=null;function v(){if(F(),f.innerHTML="",O.length===0||h===null){Tt(f,"No parametric test data available for box plots.");return}const l=Lt(),n=It(l,h),u=it.find(e=>e.testNumber===h),C=u?.unit,j=u?.limitLow,K=u?.limitHigh;if(n.every(e=>e.count===0)){Tt(f,"No parametric test data available for box plots.");return}const r=document.createElement("canvas");r.style.display="block",r.style.cursor="default",f.appendChild(r);const rt=y*2+Math.min(n.length,Yt)*(T+M)+vt;f.style.maxHeight=`${rt}px`,f.style.overflowY="auto",f.style.scrollbarGutter="stable";let A=-1;const D=window.devicePixelRatio||1,ct=n.filter(e=>e.count>0),ft=Math.min(...ct.map(e=>e.min)),dt=Math.max(...ct.map(e=>e.max)),B=_&&j!==void 0?Math.min(ft,j):ft,J=_&&K!==void 0?Math.max(dt,K):dt,mt=J-B||1,P=q&&B>0,Z=P?Math.log10(B):0,ut=(P?Math.log10(J):0)-Z||1,tt=Xt(Math.max(Math.abs(B),Math.abs(J)),C);function $t(){const e=y+Mt,o=r.clientWidth-e-Nt-y;return{plotX:e,plotMaxWidth:Math.max(10,o)}}function k(e,o,i){return P?o+(Math.log10(e)-Z)/ut*i:o+(e-B)/mt*i}function Ct(){const e=[];for(let o=0;o<=4;o++)e.push(P?Math.pow(10,Z+ut*o/4):B+mt*o/4);return e}function G(){const e=Gt(x),o=f.clientWidth,i=y*2+n.length*(T+M)+vt;r.width=Math.max(1,Math.floor(o*D)),r.height=Math.max(1,Math.floor(i*D)),r.style.width=`${o}px`,r.style.height=`${i}px`;const t=r.getContext("2d");t.setTransform(D,0,0,D,0,0),t.clearRect(0,0,o,i),t.font="11px system-ui, sans-serif",t.textBaseline="middle";const{plotX:s,plotMaxWidth:c}=$t();n.forEach((a,g)=>{const b=y+g*(T+M),w=b+T/2;g===A&&(t.fillStyle=e.bgHover,t.fillRect(0,b-M/2,o,T+M)),t.fillStyle=e.text,t.textAlign="right";const At=a.label.length>16?`${a.label.slice(0,15)}\u2026`:a.label;if(t.fillText(At,y+Mt-8,w),a.count===0){t.fillStyle=e.textMuted,t.textAlign="left",t.fillText("no data",s,w);return}const et=k(a.min,s,c),H=k(a.q1,s,c),pt=k(a.median,s,c),nt=k(a.q3,s,c),ot=k(a.max,s,c),S=b+3,R=b+T-3;t.strokeStyle=e.textMuted,t.lineWidth=1,t.beginPath(),t.moveTo(et,w),t.lineTo(H,w),t.moveTo(nt,w),t.lineTo(ot,w),t.moveTo(et,S),t.lineTo(et,R),t.moveTo(ot,S),t.lineTo(ot,R),t.stroke(),t.fillStyle=bt,t.globalAlpha=.35,t.fillRect(H,S,Math.max(1,nt-H),R-S),t.globalAlpha=1,t.strokeStyle=bt,t.strokeRect(H,S,Math.max(1,nt-H),R-S),t.strokeStyle=e.text,t.lineWidth=2,t.beginPath(),t.moveTo(pt,S),t.lineTo(pt,R),t.stroke(),t.lineWidth=1,t.fillStyle=e.textMuted,t.textAlign="left",t.fillText(`med ${gt(a.median,C,"engineering")}`,s+c+10,w)});const p=y+n.length*(T+M);t.font="10px system-ui, sans-serif";for(const[a,g]of[[j,"LSL"],[K,"USL"]]){if(a===void 0)continue;const b=k(a,s,c);t.strokeStyle=e.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(b,0),t.lineTo(b,p),t.stroke(),t.setLineDash([]),t.fillStyle=e.warnBorder,t.textAlign="center",t.textBaseline="bottom",t.fillText(g,b,p-1)}t.font="11px system-ui, sans-serif",t.strokeStyle=e.border,t.lineWidth=1,t.beginPath(),t.moveTo(s,p),t.lineTo(s+c,p),t.stroke(),t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="top";for(const a of Ct()){const g=k(a,s,c);t.beginPath(),t.moveTo(g,p),t.lineTo(g,p+4),t.stroke(),t.fillText(tt.tick(a),g,p+6)}tt.unitLabel&&(t.textAlign="left",t.font="10px system-ui, sans-serif",t.fillText(`(${tt.unitLabel})`,s+c+22,p+6),t.font="11px system-ui, sans-serif"),t.textBaseline="middle"}function xt(e){const o=Math.floor((e-y+M/2)/(T+M));return o>=0&&o<n.length?o:-1}function I(e){return gt(e,C,"engineering")}const X=!!m&&m.length>0&&d===null,ht=e=>!X&&!!W&&l[e]?.waferIndex!==void 0;r.addEventListener("mousemove",e=>{const o=r.getBoundingClientRect(),i=xt(e.clientY-o.top),t=i>=0&&n[i].count>0&&(X||ht(i));if(i!==A&&(A=i,r.style.cursor=t?"pointer":"default",G()),i>=0&&n[i].count>0){const s=n[i],c=X?`<br><em>click to see this ${Y} by wafer</em>`:ht(i)?"<br><em>click to open this wafer</em>":"";$.innerHTML=`<strong>${s.label}</strong> (${s.count} dies)<br>max ${I(s.max)}<br>q3 ${I(s.q3)}<br>median ${I(s.median)}<br>q1 ${I(s.q1)}<br>min ${I(s.min)}${c}`,$.style.display="block",Ot($,x,e.clientX,e.clientY)}else $.style.display="none"}),r.addEventListener("mouseleave",()=>{A!==-1&&(A=-1,G()),$.style.display="none"}),r.addEventListener("click",e=>{const o=r.getBoundingClientRect(),i=xt(e.clientY-o.top);if(i===-1||n[i].count===0)return;if(X){d=n[i].label,U(),F(),v();return}const t=l[i]?.waferIndex;W&&t!==void 0&&h!==null&&W(t,h)}),V?.disconnect(),V=Wt(x,()=>G()),G(),Pt(x,f,rt)}v();function Bt(l){!O.some(n=>n.testNumber===l)||l===h||(h=l,at.value=String(l),v())}return{card:x,setTest:Bt,destroy:()=>V?.disconnect()}}
1
+ import{buildTestBoxplotData as Dt}from"../../stats/boxplot.js";import{CLR as It}from"../toolbar.js";import{fmt as gt}from"../../renderer/fmt.js";import{QUANTITY as bt}from"./palette.js";import{cardShell as Ht,observeResize as Rt,makeTooltip as Wt,positionChartTooltip as Et,makeBackButton as Ot,makeTestSelect as _t,makeToggle as yt,renderEmptyState as Tt,growCardToFitContent as Pt,resolveChartCanvasColors as Gt,makeAxisFormat as Xt,PADDING as y,VALUE_WIDTH as Nt}from"./chartShell.js";const T=24,M=5,Mt=110,Yt=12,wt=20;export function renderBoxplotPanel(N){const{title:Y="Test value distribution",items:vt,testDefs:it,onSaveImage:kt,groups:u,groupLabelText:q="group",onOpen:R}=N,{card:r,heading:St,body:m,controlsRow:W}=Ht(Y,kt,N.ownerDocument);r.style.flex="0 0 auto",m.style.flex="0 0 auto",r.style.alignSelf="start";const E=it.filter(l=>l.testNumber!==void 0);let h=N.selectedTestNumber??E[0]?.testNumber??null,Q=!1,O=!1,d=null,L=null,U=null,st=null;function Lt(){const l=!u||u.length===0?"":d??"\0overview";if(U&&st===l)return U;const n=!u||u.length===0?vt:d!==null?u.find(x=>x.key===d)?.items??[]:u.map(x=>({label:x.key,dies:x.items.flatMap(C=>C.dies??[])}));return U=n,st=l,n}function z(){d!==null&&!L?(L=Ot(()=>{d=null,z(),w()},r.ownerDocument),W.appendChild(L)):d===null&&L&&(L.remove(),L=null),St.textContent=d!==null?`${Y} \u2014 ${q}: ${d}`:Y}const at=_t(E,h,l=>{h=l,w()},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:r.ownerDocument});W.appendChild(at),W.appendChild(yt("Log scale",Q,l=>{Q=l,w()},r.ownerDocument)),W.appendChild(yt("Axis includes limits",O,l=>{O=l,w()},r.ownerDocument));const F=r.ownerDocument.createElement("div");Object.assign(F.style,{color:It.label,fontSize:"11px",marginBottom:"6px"}),r.insertBefore(F,m);function V(){const l=!!u&&u.length>0&&d===null,n=[];l?n.push(`click a ${q}'s box to see it by wafer`):R&&n.push("click a box to open that wafer");const x=n.length?`${n[0][0].toUpperCase()}${n[0].slice(1)} \xB7 `:"";F.textContent=`${x}box = Q1\u2013Q3, line = median, whiskers = min/max`}V(),z();const $=Wt(r);let j=null;function w(){if(V(),m.innerHTML="",E.length===0||h===null){Tt(m,"No parametric test data available for box plots.");return}const l=Lt(),n=Dt(l,h),x=it.find(e=>e.testNumber===h),C=x?.unit,K=x?.limitLow,J=x?.limitHigh;if(n.every(e=>e.count===0)){Tt(m,"No parametric test data available for box plots.");return}const c=r.ownerDocument.createElement("canvas");c.style.display="block",c.style.cursor="default",m.appendChild(c);const rt=y*2+Math.min(n.length,Yt)*(T+M)+wt;m.style.maxHeight=`${rt}px`,m.style.overflowY="auto",m.style.scrollbarGutter="stable";let A=-1;const _=window.devicePixelRatio||1,ct=n.filter(e=>e.count>0),ft=Math.min(...ct.map(e=>e.min)),mt=Math.max(...ct.map(e=>e.max)),B=O&&K!==void 0?Math.min(ft,K):ft,Z=O&&J!==void 0?Math.max(mt,J):mt,dt=Z-B||1,P=Q&&B>0,tt=P?Math.log10(B):0,ut=(P?Math.log10(Z):0)-tt||1,et=Xt(Math.max(Math.abs(B),Math.abs(Z)),C);function $t(){const e=y+Mt,o=c.clientWidth-e-Nt-y;return{plotX:e,plotMaxWidth:Math.max(10,o)}}function v(e,o,i){return P?o+(Math.log10(e)-tt)/ut*i:o+(e-B)/dt*i}function Ct(){const e=[];for(let o=0;o<=4;o++)e.push(P?Math.pow(10,tt+ut*o/4):B+dt*o/4);return e}function G(){const e=Gt(r),o=m.clientWidth,i=y*2+n.length*(T+M)+wt;c.width=Math.max(1,Math.floor(o*_)),c.height=Math.max(1,Math.floor(i*_)),c.style.width=`${o}px`,c.style.height=`${i}px`;const t=c.getContext("2d");t.setTransform(_,0,0,_,0,0),t.clearRect(0,0,o,i),t.font="11px system-ui, sans-serif",t.textBaseline="middle";const{plotX:s,plotMaxWidth:f}=$t();n.forEach((a,g)=>{const b=y+g*(T+M),k=b+T/2;g===A&&(t.fillStyle=e.bgHover,t.fillRect(0,b-M/2,o,T+M)),t.fillStyle=e.text,t.textAlign="right";const At=a.label.length>16?`${a.label.slice(0,15)}\u2026`:a.label;if(t.fillText(At,y+Mt-8,k),a.count===0){t.fillStyle=e.textMuted,t.textAlign="left",t.fillText("no data",s,k);return}const nt=v(a.min,s,f),I=v(a.q1,s,f),pt=v(a.median,s,f),ot=v(a.q3,s,f),lt=v(a.max,s,f),S=b+3,H=b+T-3;t.strokeStyle=e.textMuted,t.lineWidth=1,t.beginPath(),t.moveTo(nt,k),t.lineTo(I,k),t.moveTo(ot,k),t.lineTo(lt,k),t.moveTo(nt,S),t.lineTo(nt,H),t.moveTo(lt,S),t.lineTo(lt,H),t.stroke(),t.fillStyle=bt,t.globalAlpha=.35,t.fillRect(I,S,Math.max(1,ot-I),H-S),t.globalAlpha=1,t.strokeStyle=bt,t.strokeRect(I,S,Math.max(1,ot-I),H-S),t.strokeStyle=e.text,t.lineWidth=2,t.beginPath(),t.moveTo(pt,S),t.lineTo(pt,H),t.stroke(),t.lineWidth=1,t.fillStyle=e.textMuted,t.textAlign="left",t.fillText(`med ${gt(a.median,C,"engineering")}`,s+f+10,k)});const p=y+n.length*(T+M);t.font="10px system-ui, sans-serif";for(const[a,g]of[[K,"LSL"],[J,"USL"]]){if(a===void 0)continue;const b=v(a,s,f);t.strokeStyle=e.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(b,0),t.lineTo(b,p),t.stroke(),t.setLineDash([]),t.fillStyle=e.warnBorder,t.textAlign="center",t.textBaseline="bottom",t.fillText(g,b,p-1)}t.font="11px system-ui, sans-serif",t.strokeStyle=e.border,t.lineWidth=1,t.beginPath(),t.moveTo(s,p),t.lineTo(s+f,p),t.stroke(),t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="top";for(const a of Ct()){const g=v(a,s,f);t.beginPath(),t.moveTo(g,p),t.lineTo(g,p+4),t.stroke(),t.fillText(et.tick(a),g,p+6)}et.unitLabel&&(t.textAlign="left",t.font="10px system-ui, sans-serif",t.fillText(`(${et.unitLabel})`,s+f+22,p+6),t.font="11px system-ui, sans-serif"),t.textBaseline="middle"}function xt(e){const o=Math.floor((e-y+M/2)/(T+M));return o>=0&&o<n.length?o:-1}function D(e){return gt(e,C,"engineering")}const X=!!u&&u.length>0&&d===null,ht=e=>!X&&!!R&&l[e]?.waferIndex!==void 0;c.addEventListener("mousemove",e=>{const o=c.getBoundingClientRect(),i=xt(e.clientY-o.top),t=i>=0&&n[i].count>0&&(X||ht(i));if(i!==A&&(A=i,c.style.cursor=t?"pointer":"default",G()),i>=0&&n[i].count>0){const s=n[i],f=X?`<br><em>click to see this ${q} by wafer</em>`:ht(i)?"<br><em>click to open this wafer</em>":"";$.innerHTML=`<strong>${s.label}</strong> (${s.count} dies)<br>max ${D(s.max)}<br>q3 ${D(s.q3)}<br>median ${D(s.median)}<br>q1 ${D(s.q1)}<br>min ${D(s.min)}${f}`,$.style.display="block",Et($,r,e.clientX,e.clientY)}else $.style.display="none"}),c.addEventListener("mouseleave",()=>{A!==-1&&(A=-1,G()),$.style.display="none"}),c.addEventListener("click",e=>{const o=c.getBoundingClientRect(),i=xt(e.clientY-o.top);if(i===-1||n[i].count===0)return;if(X){d=n[i].label,z(),V(),w();return}const t=l[i]?.waferIndex;R&&t!==void 0&&h!==null&&R(t,h)}),j?.disconnect(),j=Rt(r,()=>G()),G(),Pt(r,m,rt)}w();function Bt(l){!E.some(n=>n.testNumber===l)||l===h||(h=l,at.value=String(l),w())}return{card:r,setTest:Bt,destroy:()=>j?.disconnect()}}
@@ -21,6 +21,11 @@ export interface CapabilityPanelOptions {
21
21
  key: string;
22
22
  items: CapabilityItem[];
23
23
  }[];
24
+ /** Document to build this panel's DOM into. Default `document` — pass the
25
+ * host's own `ownerDocument` when the container might live in a
26
+ * different document (e.g. a gallery card detached into its own popup
27
+ * window). */
28
+ ownerDocument?: Document;
24
29
  }
25
30
  export interface CapabilityPanelHandle {
26
31
  card: HTMLElement;
@@ -1 +1 @@
1
- import{buildCapabilityData as ct}from"../../stats/capability.js";import{capabilityColor as dt}from"./palette.js";import{CLR as V}from"../toolbar.js";import{cardShell as pt,chartFillHeight as mt,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 E(w){return w===null?"\u2014":w.toFixed(2)}export function renderCapabilityPanel(w){const{title:K="Process capability",items:Z,testDefs:H,onSaveImage:tt,onSelectTest:R,groups:p}=w,{card:y,body:m,controlsRow:et}=pt(K,tt),nt=H.some(e=>e.testNumber!==void 0);m.style.overflowX="auto";let B=p&&p.length>0?p[0].key:void 0;p&&p.length>0&&et.appendChild(bt("Group:",p.map(e=>({value:e.key,label:e.key})),B??"",e=>{B=e,O()}));function ot(){return!p||p.length===0?Z:p.find(e=>e.key===B)?.items??[]}const A=document.createElement("div");Object.assign(A.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),y.insertBefore(A,m);const M=window.devicePixelRatio||1,k=ft(y);let D=()=>{};function lt(e,n,h){A.innerHTML="";const c=document.createElement("span");Object.assign(c.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:V.value,fontSize:"12px",fontWeight:"500"});const S=h-e,C=n>0?` \xB7 ${n} without spec limits`:"",T=document.createElement("span");T.textContent=S>0?`${e} of ${h} tests shown${C} \xB7 ${S} excluded (no recorded values)`:`${e} test${e!==1?"s":""} shown${C}`,c.appendChild(T);const L=document.createElement("span");L.textContent="\u24D8",L.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(L.style,{color:V.label,cursor:"help",fontSize:"12px"}),c.appendChild(L),A.appendChild(c)}function it(e){if(m.innerHTML="",e.length===0)return xt(m,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const n=document.createElement("canvas");n.style.display="block",n.style.cursor="default",m.appendChild(n);const h=e.length,c=Math.min(0,...e.map(a=>a.min)),S=Math.max(1,...e.map(a=>a.max)),C=(S-c)*.05||.1,T=c-C,st=S+C-T||1;function X(a){return Math.max(Mt,Math.min(kt,Math.floor(a/h)))}function b(a,o,l){return o+(1-(a-T)/st)*l}let $=-1;function N(){ht(n);const a=m.clientWidth,o=X(a),l=o*h,i=J,x=l,u=mt(y,m,n,J+200+Y),_=Math.max(i+60,u-Y),f=_-i;n.width=Math.max(1,Math.floor(x*M)),n.height=Math.max(1,Math.floor(u*M)),n.style.width=`${x}px`,n.style.height=`${u}px`;const t=n.getContext("2d");t.setTransform(M,0,0,M,0,0),t.clearRect(0,0,x,u);const v=vt(y);e.forEach((r,j)=>{const g=j*o,s=g+o/2,d=Math.max(4,o*.55);if(j===$&&(t.fillStyle=v.bgHover,t.fillRect(g,i,o,_-i)),r.hasSpec){const q=b(0,i,f),Q=b(1,i,f);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=r.hasSpec?dt(r.ppk):v.textMuted,z=b(r.min,i,f),P=b(r.q1,i,f),U=b(r.median,i,f),W=b(r.q3,i,f),I=b(r.max,i,f);t.strokeStyle=v.textMuted,t.lineWidth=1,t.beginPath(),t.moveTo(s,z),t.lineTo(s,P),t.moveTo(s,W),t.lineTo(s,I),t.moveTo(s-d/4,z),t.lineTo(s+d/4,z),t.moveTo(s-d/4,I),t.lineTo(s+d/4,I),t.stroke(),t.fillStyle=G,t.globalAlpha=.5,t.fillRect(s-d/2,Math.min(P,W),d,Math.max(1,Math.abs(W-P))),t.globalAlpha=1,t.strokeStyle=G,r.hasSpec||t.setLineDash([4,2]),t.strokeRect(s-d/2,Math.min(P,W),d,Math.max(1,Math.abs(W-P))),t.setLineDash([]),t.strokeStyle=v.text,t.lineWidth=2,t.beginPath(),t.moveTo(s-d/2,U),t.lineTo(s+d/2,U),t.stroke(),t.lineWidth=1;const rt=r.label.length>12?`${r.label.slice(0,11)}\u2026`:r.label;t.save(),t.translate(s,_+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,f)}function F(a){const o=n.getBoundingClientRect(),l=m.clientWidth,i=X(l),x=(a.clientX-o.left)*(n.width/M/o.width),u=Math.floor(x/i);return u>=0&&u<h?u:-1}return n.addEventListener("mousemove",a=>{const o=F(a);if(o!==$&&($=o,N()),o===-1){k.style.display="none";return}const l=e[o],i=x=>gt(x,l.unit);k.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 ${E(l.cp)} \xB7 Cpk ${E(l.cpk)}<br>Pp ${E(l.pp)} \xB7 Ppk ${E(l.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${i(l.mean)} \xB7 stddev ${i(l.stdOverall)}`)+(R?"<br><em>click to view in boxplot</em>":""),k.style.display="block",yt(k,y,a.clientX,a.clientY),n.style.cursor=R?"pointer":"default"}),n.addEventListener("mouseleave",()=>{$!==-1&&($=-1,N()),k.style.display="none"}),n.addEventListener("click",a=>{if(!R)return;const o=F(a);o!==-1&&R(e[o].testNumber)}),N}function O(){const e=ct(ot(),H),n=H.filter(c=>c.testNumber!==void 0).length,h=e.filter(c=>!c.hasSpec).length;lt(e.length,h,n),D=it(e),D()}const at=ut(y,()=>D());return O(),{card:y,hasData:nt,destroy:()=>at.disconnect()}}
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()}}
@@ -103,9 +103,9 @@ export interface CardShell {
103
103
  controlsRow: HTMLElement;
104
104
  body: HTMLElement;
105
105
  }
106
- export declare function cardShell(title: string, onSaveImage?: SaveImageHandler): CardShell;
107
- export declare function makeSegmented(options: Array<[value: string, label: string]>, current: string, onChange: (value: string) => void): HTMLElement;
108
- export declare function makeBackButton(onBack: () => void): HTMLButtonElement;
106
+ export declare function cardShell(title: string, onSaveImage?: SaveImageHandler, ownerDocument?: Document): CardShell;
107
+ export declare function makeSegmented(options: Array<[value: string, label: string]>, current: string, onChange: (value: string) => void, ownerDocument?: Document): HTMLElement;
108
+ export declare function makeBackButton(onBack: () => void, ownerDocument?: Document): HTMLButtonElement;
109
109
  export declare function renderEmptyState(body: HTMLElement, message: string, styleOverrides?: Partial<CSSStyleDeclaration>): void;
110
110
  export interface TestSelectItem {
111
111
  testNumber: number;
@@ -125,6 +125,7 @@ export interface TestSelectItem {
125
125
  export declare function makeTestSelect(testOptions: readonly TestSelectItem[], selected: number | null, onChange: (testNumber: number) => void, opts?: {
126
126
  maxWidth?: string;
127
127
  emptyText?: string;
128
+ ownerDocument?: Document;
128
129
  }): HTMLElement & {
129
130
  value: string;
130
131
  };
@@ -134,16 +135,18 @@ export interface WaferSelectItem {
134
135
  export declare function makeWaferSelect(items: readonly WaferSelectItem[], selectedIndex: number | null, onChange: (index: number | null) => void, opts?: {
135
136
  maxWidth?: string;
136
137
  allLabel?: string;
138
+ ownerDocument?: Document;
137
139
  }): HTMLSelectElement;
138
- export declare function makeToggle(labelText: string, checked: boolean, onChange: (v: boolean) => void): HTMLLabelElement;
140
+ export declare function makeToggle(labelText: string, checked: boolean, onChange: (v: boolean) => void, ownerDocument?: Document): HTMLLabelElement;
139
141
  export declare function makeLabeledSelect(labelText: string, options: readonly {
140
142
  value: string;
141
143
  label: string;
142
144
  }[], selected: string, onChange: (value: string) => void, opts?: {
143
145
  maxWidth?: string;
144
146
  hook?: string;
147
+ ownerDocument?: Document;
145
148
  }): HTMLLabelElement;
146
- export declare function makeChartGridWrap(): HTMLDivElement;
149
+ export declare function makeChartGridWrap(ownerDocument?: Document): HTMLDivElement;
147
150
  /** Small positioned hover tooltip, matching the pattern each ported panel builds for itself. */
148
151
  export declare function makeTooltip(card: HTMLElement): HTMLElement;
149
152
  /**
@@ -1 +1 @@
1
- import{CLR as a,Z_BASE as T,MENU_SEARCH_THRESHOLD as N,makeMenuSearchBox as M,markMenuTrigger as E,saveImageBlob as D,openReparentedModal as I}from"../toolbar.js";import{ICONS as L}from"../icons.js";import{fmt as z,fmtColorbarAxis as W}from"../../renderer/fmt.js";export function openChartExpandModal(e,n,o){const t=e.getAttribute("style")??"";if(Object.assign(e.style,{flex:"1",minHeight:"0",border:"none",borderRadius:"0"}),!I([e],{title:n,onClosed:()=>{e.setAttribute("style",t),o&&(o.style.display="flex")}})){e.setAttribute("style",t);return}o&&(o.style.display="none")}const F={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040"},O={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border"};export function resolveChartCanvasColors(e){const n=getComputedStyle(e),o={};for(const t of Object.keys(O)){const i=n.getPropertyValue(`--wmap-${O[t]}`).trim();o[t]=i||F[t]}return o}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,n){const o=new ResizeObserver(n);return o.observe(e),{disconnect:()=>o.disconnect()}}export function growCardToFitContent(e,n,o){const t=e.offsetHeight-n.clientHeight;e.style.minHeight=`${t+o}px`}export function chartFillHeight(e,n,o,t){let i=0;for(const l of Array.from(n.children))l!==o&&(i+=l.offsetHeight);return growCardToFitContent(e,n,i+t),Math.max(t,n.clientHeight-i)}export function applyCanvasFlow(e,n=0){e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${n}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:z(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,n){if(!n)return{tick:formatValue,unitLabel:""};const{tickFmt:o,axisLabel:t}=W(e,null,n);return{tick:o,unitLabel:t}}export function drawAxisUnit(e,n,o,t,i){const l={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(`(${n})`,o,t),Object.assign(e,l)}export function saveCanvasPng(e,n,o){const t=document.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(l=>{l&&D(l,n,o)},"image/png")}export function cardShell(e,n){const o=document.createElement("div");o.dataset.wmapChartCard="1",o.dataset.wmapChartTitle=e,Object.assign(o.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 t=document.createElement("div");Object.assign(t.style,{display:"flex",alignItems:"center",gap:"6px",marginBottom:"6px"});const i=document.createElement("div");i.textContent=e,Object.assign(i.style,{color:a.value,fontSize:"13px",fontWeight:"600",flex:"1"}),t.appendChild(i);const l=document.createElement("button");l.type="button",l.title="Save as PNG",l.setAttribute("aria-label","Save as PNG"),l.innerHTML=L.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 p=o.querySelector("canvas");p&&saveCanvasPng(p,e,n)}),t.appendChild(l);const r=document.createElement("button");r.type="button",r.title="Expand",r.setAttribute("aria-label","Expand"),r.innerHTML=L.expand,Object.assign(r.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"}),r.addEventListener("click",()=>openChartExpandModal(o,i.textContent??e,r)),t.appendChild(r),o.appendChild(t);const c=document.createElement("div");Object.assign(c.style,{display:"flex",gap:"6px",marginBottom:"6px",flexWrap:"wrap",alignItems:"center"}),o.appendChild(c);const d=document.createElement("div");return Object.assign(d.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),o.appendChild(d),{card:o,heading:i,controlsRow:c,body:d}}export function makeSegmented(e,n,o){const t=document.createElement("div");t.setAttribute("role","radiogroup"),Object.assign(t.style,{display:"inline-flex",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",overflow:"hidden"});const i=`seg-${Math.random().toString(36).slice(2,9)}`,l=[];return e.forEach(([r,c],d)=>{const p=document.createElement("label");Object.assign(p.style,{display:"inline-flex",alignItems:"center",fontSize:"12px",padding:"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:d>0?`1px solid ${a.menuBorder}`:"none"});const s=document.createElement("input");s.type="radio",s.name=i,s.value=r,s.checked=r===n,Object.assign(s.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const g=()=>{p.style.background=s.checked?a.bgActive:a.menuBg,p.style.color=s.checked?a.iconActive:a.text};l.push(g),g(),s.addEventListener("change",()=>{if(s.checked){for(const h of l)h();o(r)}}),p.append(s,document.createTextNode(c)),t.appendChild(p)}),t}export function makeBackButton(e){const n=document.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,n,o){const t=document.createElement("div");t.textContent=n,Object.assign(t.style,{color:a.label,fontSize:"12px",padding:"8px 0"}),o&&Object.assign(t.style,o),e.appendChild(t)}export function makeTestSelect(e,n,o,t={}){const{maxWidth:i="200px",emptyText:l="No parametric tests"}=t;if(e.length>N)return P(e,n,o,i);const r=document.createElement("select");if(Object.assign(r.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:i}),e.length===0){r.disabled=!0;const c=document.createElement("option");c.textContent=l,r.appendChild(c)}else{for(const c of e){const d=document.createElement("option");d.value=String(c.testNumber),d.textContent=c.name||`Test ${c.testNumber}`,c.testNumber===n&&(d.selected=!0),r.appendChild(d)}r.addEventListener("change",()=>o(Number(r.value)))}return r}function P(e,n,o,t){const i=f=>e.find(y=>y.testNumber===f)?.name||`Test ${f}`;let l=n;const r=document.createElement("button");r.type="button",Object.assign(r.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(r,!1);const c=document.createElement("span");Object.assign(c.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const d=document.createElement("span");d.textContent="\u25BE",d.style.flex="0 0 auto",r.append(c,d);const p=()=>{c.textContent=l!==null?i(l):"(none)"};p();let s=null;const g=()=>{s&&(s.remove(),s=null,E(r,!1))},h=()=>{g(),r.focus()};function j(){const f=r.getBoundingClientRect(),y=r.ownerDocument.defaultView??window,C=Math.max(f.width,220),R=320,$=Math.min(f.left,Math.max(4,(y.innerWidth??1/0)-C-4));s=document.createElement("div"),Object.assign(s.style,{position:"fixed",top:`${f.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:`${C}px`,maxHeight:`${R}px`,overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const v=[],S=()=>v.filter(u=>u.row.style.display!=="none").map(u=>u.row),k=M(u=>{for(const m of v)m.row.style.display=m.label.includes(u)?"":"none"},"Filter tests\u2026");Object.assign(k.style,{position:"sticky",top:"0",zIndex:"1",background:a.menuBg}),k.addEventListener("keydown",u=>{if(u.key==="Escape"){h();return}u.key==="ArrowDown"&&(u.preventDefault(),S()[0]?.focus())}),s.appendChild(k);for(const u of e){const m=u.name||`Test ${u.testNumber}`,x=u.testNumber===l,b=document.createElement("div");b.textContent=m,b.setAttribute("role","menuitemradio"),b.setAttribute("aria-checked",x?"true":"false"),b.tabIndex=-1,Object.assign(b.style,{padding:"6px 14px",fontSize:"12px",cursor:"pointer",color:x?a.iconActive:a.text,fontWeight:x?"700":"400",background:x?a.menuActive:"transparent",whiteSpace:"nowrap",outline:"none"});const B=()=>{x||(b.style.background=a.menuHover)},A=()=>{b.style.background=x?a.menuActive:"transparent"};b.addEventListener("mouseenter",B),b.addEventListener("mouseleave",A),b.addEventListener("focus",B),b.addEventListener("blur",A),b.addEventListener("click",H=>{H.stopPropagation(),l=u.testNumber,p(),h(),o(u.testNumber)}),s.appendChild(b),v.push({row:b,label:m.toLowerCase()})}s.addEventListener("keydown",u=>{const m=S();if(m.length===0)return;const x=m.indexOf(document.activeElement);switch(u.key){case"ArrowDown":u.preventDefault(),m[x<0||x===m.length-1?0:x+1].focus();break;case"ArrowUp":u.preventDefault(),m[x<=0?m.length-1:x-1].focus();break;case"Home":u.preventDefault(),m[0].focus();break;case"End":u.preventDefault(),m[m.length-1].focus();break;case"Enter":case" ":x>=0&&(u.preventDefault(),m[x].click());break;case"Escape":case"Tab":u.preventDefault(),h();break}}),s.setAttribute("role","menu"),s.setAttribute("aria-label","Test"),r.ownerDocument.body.appendChild(s),E(r,!0)}r.addEventListener("click",f=>{if(f.stopPropagation(),s){g();return}j()});const w=f=>{if(!r.isConnected){r.ownerDocument.removeEventListener("click",w);return}s&&!s.contains(f.target)&&!r.contains(f.target)&&g()};return r.ownerDocument.addEventListener("click",w),Object.defineProperty(r,"value",{get:()=>l!==null?String(l):"",set:f=>{l=f===""?null:Number(f),p()}})}export function makeWaferSelect(e,n,o,t={}){const{maxWidth:i="160px",allLabel:l="All wafers"}=t,r="\0all",c=document.createElement("select");Object.assign(c.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:i});const d=document.createElement("option");return d.value=r,d.textContent=l,n===null&&(d.selected=!0),c.appendChild(d),e.forEach((p,s)=>{const g=document.createElement("option");g.value=String(s),g.textContent=p.label??`#${s}`,n===s&&(g.selected=!0),c.appendChild(g)}),c.addEventListener("change",()=>o(c.value===r?null:Number(c.value))),c}export function makeToggle(e,n,o){const t=document.createElement("label");Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:a.label,cursor:"pointer",userSelect:"none"});const i=document.createElement("input");return i.type="checkbox",i.checked=n,i.style.cssText="margin:0;cursor:pointer;",i.addEventListener("change",()=>o(i.checked)),t.append(i,document.createTextNode(e)),t}export function makeLabeledSelect(e,n,o,t,i={}){const{maxWidth:l="160px",hook:r}=i,c=document.createElement("label");c.textContent=e,Object.assign(c.style,{color:a.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const d=document.createElement("select");r&&(d.dataset.wmapSelect=r),Object.assign(d.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:l});for(const p of n){const s=document.createElement("option");s.value=p.value,s.textContent=p.label,p.value===o&&(s.selected=!0),d.appendChild(s)}return d.addEventListener("change",()=>t(d.value)),c.appendChild(d),c}export function makeChartGridWrap(){const e=document.createElement("div");return e.dataset.wmapChartGrid="1",Object.assign(e.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:"10px",flex:"0 0 auto"}),e}export function makeTooltip(e){const n=document.createElement("div");return Object.assign(n.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(n),n}export function positionChartTooltip(e,n,o,t){const i=n.getBoundingClientRect(),l=14,r=4;e.style.left="0",e.style.top="0";const c=e.offsetWidth,d=e.offsetHeight;let p=o-i.left+l,s=t-i.top+l;p+c+r>i.width&&(p=o-i.left-l-c),s+d+r>i.height&&(s=t-i.top-l-d),p=Math.max(r,p),s=Math.max(r,s),e.style.left=`${p}px`,e.style.top=`${s}px`}
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`}
@@ -25,6 +25,11 @@ export interface CorrelationPanelOptions {
25
25
  }[];
26
26
  /** Clicking a non-diagonal cell calls this — the Analysis tab wires it to drive the scatter panel's X/Y in place. */
27
27
  onSelectPair?: (xTestNumber: number, yTestNumber: number) => void;
28
+ /** Document to build this panel's DOM into. Default `document` — pass the
29
+ * host's own `ownerDocument` when the container might live in a
30
+ * different document (e.g. a gallery card detached into its own popup
31
+ * window). */
32
+ ownerDocument?: Document;
28
33
  }
29
34
  export interface CorrelationPanelHandle {
30
35
  card: HTMLElement;
@@ -1 +1 @@
1
- import{buildCorrelationMatrix as ft,filterCorrelationMatrix as ut}from"../../stats/correlation.js";import{CORRELATION_POSITIVE as et,CORRELATION_NEGATIVE as nt}from"./palette.js";import{buildFacetTable as ht}from"../../stats/facets.js";import{CLR as x}from"../toolbar.js";import{cardShell as mt,observeResize as xt,makeTooltip as gt,positionChartTooltip as bt,makeLabeledSelect as yt,makeWaferSelect as Mt,renderEmptyState as Lt,resolveChartCanvasColors as vt}from"./chartShell.js";const st=5,ot=100,lt=20;function it(T){const c=T.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(c)return[+c[1],+c[2],+c[3]];const r=T.trim().replace("#","");return r.length===6?[parseInt(r.slice(0,2),16),parseInt(r.slice(2,4),16),parseInt(r.slice(4,6),16)]:null}function It(T,c,r){const k=it(T);if(!k)return T;const P=Math.round(c[0]+(k[0]-c[0])*r),u=Math.round(c[1]+(k[1]-c[1])*r),A=Math.round(c[2]+(k[2]-c[2])*r);return`rgb(${P},${u},${A})`}export function renderCorrelationPanel(T){const{title:c="Test correlation matrix",items:r,testDefs:k,onSaveImage:P,groups:u,onSelectPair:A}=T,{card:$,body:w,controlsRow:V}=mt(c,P);w.style.overflowX="auto",$.style.alignSelf="start";let F=lt,U=u&&u.length>0?u[0].key:void 0,j=null;const H=document.createElement("label");H.textContent="Max tests:",H.title="Cap on how many tests the matrix includes (strongest correlations kept first)",Object.assign(H.style,{color:x.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const L=document.createElement("input");L.type="number",L.min=String(st),L.max=String(ot),L.value=String(F),Object.assign(L.style,{width:"52px",fontSize:"12px",padding:"2px 4px",background:x.menuBg,color:x.value,border:`1px solid ${x.menuBorder}`,borderRadius:"3px"}),L.addEventListener("change",()=>{const e=Math.max(st,Math.min(ot,parseInt(L.value,10)||lt));L.value=String(e),e!==F&&(F=e,G())}),H.appendChild(L),V.appendChild(H),u&&u.length>0?V.appendChild(yt("Group:",u.map(e=>({value:e.key,label:e.key})),U??"",e=>{U=e,G()})):r.length>1&&V.appendChild(Mt(r,j,e=>{j=e,G()}));function rt(){return u&&u.length>0?u.find(e=>e.key===U)?.items??[]:j!==null?r[j]?[r[j]]:[]:r}const B=document.createElement("div");Object.assign(B.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),$.insertBefore(B,w);const W=window.devicePixelRatio||1,S=gt($);let q=()=>{};function at(e,o,a,f,I){if(B.innerHTML="",I.length>0){const b=document.createElement("div");b.textContent=`\u26A0 Mixed ${I.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(b.style,{color:x.warnText,background:x.warnBg,border:`1px solid ${x.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px"}),B.appendChild(b)}const i=document.createElement("span");Object.assign(i.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:x.label,fontSize:"11px",flexWrap:"wrap"});const C=document.createElement("span");C.textContent="Pearson r \xB7 click a cell to view that pair in scatter \xB7",i.appendChild(C);const d=document.createElement("span");Object.assign(d.style,{display:"inline-flex",alignItems:"center",gap:"4px"});const O=document.createElement("span");O.textContent="\u22121";const g=document.createElement("span");Object.assign(g.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:"2px",border:`1px solid ${x.menuBorder}`,background:`linear-gradient(to right, ${nt}, ${x.menuBg}, ${et})`});const _=document.createElement("span");_.textContent="+1",d.append(O,g,_),i.appendChild(d),B.appendChild(i);const E=document.createElement("span");if(Object.assign(E.style,{color:x.value,fontSize:"12px",fontWeight:"500"}),e===0&&o===0)E.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 b=[];e>0&&b.push(`${e} strong (|r| \u2265 0.7)`),o>0&&b.push(`${o} moderate (0.4\u20130.7)`);const X=e+o;let z=b.join(", ")+` pair${X!==1?"s":""} found`;a>0&&(z+=` \xB7 ${a} weak pair${a!==1?"s":""} not shown`),E.textContent=z}B.appendChild(E)}function ct(e){if(w.innerHTML="",e.tests.length<2)return Lt(w,"Need at least two parametric tests for a correlation matrix."),()=>{};const o=document.createElement("canvas");o.style.display="block",o.style.cursor="default",w.appendChild(o);const a=e.tests.length,f=s=>s.label.split(" (#")[0],I=Math.min(14,Math.max(...e.tests.map(s=>f(s).length))),i=I*6.5+8,C=10,d=Math.round(C*6.5*Math.sin(Math.PI/4))+14,O=14,g=26;let _=-1,E=-1;function b(s){const l=Math.max(0,s-i);return Math.max(O,Math.min(g,Math.floor(l/a)))}const X=new Map;for(const s of e.cells){let l=X.get(s.yIndex);l||(l=[],X.set(s.yIndex,l)),l.push(s)}function z(){const s=vt($),l=it(s.bg)??[255,255,255],p=w.clientWidth,n=b(p),N=n*a,v=d+n*a+4,y=i+N;o.width=Math.max(1,Math.floor(y*W)),o.height=Math.max(1,Math.floor(v*W)),o.style.width=`${y}px`,o.style.height=`${v}px`;const t=o.getContext("2d");t.setTransform(W,0,0,W,0,0),t.clearRect(0,0,y,v),t.font="10px system-ui, sans-serif",t.fillStyle=s.text,e.tests.forEach((h,m)=>{const M=f(h),R=M.length>C?`${M.slice(0,C-1)}\u2026`:M,J=i+m*n+n/2;t.save(),t.translate(J,d-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=m===_?s.text:s.textMuted,t.fillText(R,0,0),t.restore()}),e.tests.forEach((h,m)=>{const M=f(h),R=d+m*n,J=R+n/2;t.font="10px system-ui, sans-serif",t.textAlign="right",t.textBaseline="middle",t.fillStyle=m===E?s.text:s.textMuted,t.fillText(M.length>I?`${M.slice(0,I-1)}\u2026`:M,i-4,J),(X.get(m)??[]).forEach(Z=>{const K=Z.xIndex,Y=i+K*n,pt=K===_&&m===E,tt=K===m,D=Z.r;if(D===null){t.fillStyle=s.border,t.globalAlpha=.4,t.fillRect(Y+1,R+1,n-2,n-2),t.globalAlpha=1;return}tt?t.fillStyle=s.bgHover:t.fillStyle=It(D>=0?et:nt,l,Math.abs(D)),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(D)>.6?s.bg:s.text,t.textAlign="center",t.textBaseline="middle",t.fillText(D.toFixed(2),Y+n/2,R+n/2)),pt&&(t.strokeStyle=s.text,t.lineWidth=2,t.strokeRect(Y+1,R+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=s.bg,t.lineWidth=1;for(let h=0;h<=a;h++){const m=i+h*n;t.beginPath(),t.moveTo(m,d),t.lineTo(m,d+a*n),t.stroke();const M=d+h*n;t.beginPath(),t.moveTo(i,M),t.lineTo(i+a*n,M),t.stroke()}}function Q(s){const l=o.getBoundingClientRect(),p=w.clientWidth,n=b(p),N=(s.clientX-l.left)*(o.width/W/l.width),v=(s.clientY-l.top)*(o.height/W/l.height),y=Math.floor((N-i)/n),t=Math.floor((v-d)/n);return y>=0&&y<a&&t>=0&&t<a?{xi:y,yi:t}:null}return o.addEventListener("mousemove",s=>{const l=Q(s);if(!l){S.style.display="none",o.style.cursor="default";return}const{xi:p,yi:n}=l,N=p===n;o.style.cursor=N||!A?"default":"pointer";const v=e.cells.find(h=>h.xIndex===p&&h.yIndex===n),y=e.tests[p].label,t=e.tests[n].label;N?S.innerHTML=`<strong>${y}</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[p])}</strong> (#${e.tests[p].testNumber})<br>r = ${v.r.toFixed(4)}${A?"<br><em>click to view in scatter</em>":""}`:S.innerHTML=`${t} vs ${y}<br><em>insufficient data</em>`,S.style.display="block",bt(S,$,s.clientX,s.clientY)}),o.addEventListener("mouseleave",()=>{S.style.display="none"}),o.addEventListener("click",s=>{const l=Q(s);if(!l||!A)return;const{xi:p,yi:n}=l;p!==n&&(_=p,E=n,z(),A(e.tests[p].testNumber,e.tests[n].testNumber))}),z}function G(){const e=rt(),o=e.flatMap(g=>g.dies??[]),a=ft(o,k),{matrix:f,strongPairs:I,moderatePairs:i,hiddenWeakPairs:C,strongestPair:d}=ut(a,{minTests:6,maxTests:F}),O=ht(e,{facetableOnly:!0}).filter(g=>g.splittable).map(g=>g.label);at(I,i,C,d,O),q=ct(f),q()}const dt=xt($,()=>q());return G(),{card:$,destroy:()=>dt.disconnect()}}
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()}}
@@ -19,6 +19,11 @@ export interface HistogramPanelOptions {
19
19
  key: string;
20
20
  items: HistogramItem[];
21
21
  }[];
22
+ /** Document to build this panel's DOM into. Default `document` — pass the
23
+ * host's own `ownerDocument` when the container might live in a
24
+ * different document (e.g. a gallery card detached into its own popup
25
+ * window). */
26
+ ownerDocument?: Document;
22
27
  }
23
28
  export interface HistogramPanelHandle {
24
29
  card: HTMLElement;
@@ -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 Et,observeResize as xt,makeTooltip 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 vt,PADDING as tt}from"./chartShell.js";const mt=230,St=36,at=18;function kt(s,R,N,z,rt,I,b,u=4){const j=Math.max(1,I/u),F=Math.pow(10,Math.floor(Math.log10(j))),f=j/F,G=(f<=1?1:f<=2?2:f<=5?5:10)*F,$=z-N;s.save(),s.font="10px system-ui, sans-serif",s.textAlign="right",s.textBaseline="middle";for(let K=0;K<=I+1e-9;K+=G){const _=z-K/(I||1)*$;s.strokeStyle=b.grid,s.lineWidth=1,s.beginPath(),s.moveTo(R,_),s.lineTo(R+rt,_),s.stroke(),s.fillStyle=b.text,s.fillText(String(Math.round(K)),R-4,_)}s.strokeStyle=b.axis,s.lineWidth=1,s.beginPath(),s.moveTo(R,N),s.lineTo(R,z),s.stroke(),s.restore()}export function renderHistogramPanel(s){const{title:R="Test value distribution",items:N,testDefs:z,onSaveImage:rt,groups:I}=s,{card:b,body:u,controlsRow:j}=Et(R,rt),F=z.filter(a=>a.testNumber!==void 0);let f=s.selectedTestNumber??F[0]?.testNumber??null,G=null,$=!1;const K=$t(F,f,a=>{f=a,U()},{maxWidth:"200px"});j.appendChild(K);const _=Bt(N,G,a=>{G=a,U()});j.appendChild(_),j.appendChild(Rt("Axis includes limits",$,a=>{$=a,U()}));const A=Pt(b);let J=null,y=null;function et(a){const w=z.find(O=>O.testNumber===a);return{unit:w?.unit,limitLow:w?.limitLow,limitHigh:w?.limitHigh}}function U(){if(J?.disconnect(),J=null,u.innerHTML="",f===null){pt(u,"No parametric test data available for a histogram.");return}const a=I&&I.length>0?At(I,f,16,$?et(f).limitLow:void 0,$?et(f).limitHigh:void 0):null;if(_.style.display=a?"none":"",a){if(a.series.length===0){pt(u,"No parametric test data available for a histogram.");return}Lt(a);return}const w=G!==null?[N[G]]:N,{unit:O,limitLow:nt,limitHigh:x}=et(f),i=Ht(w,f,16,$?nt:void 0,$?x:void 0);if(i.length===0){pt(u,"No parametric test data available for a histogram.");return}const D=Math.max(...i.map(p=>p.count),1),P=document.createElement("div");Object.assign(P.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),P.textContent=`max ${D} dies/bucket`,u.appendChild(P);const h=document.createElement("canvas");h.style.display="block",h.style.cursor="default",u.appendChild(h);let v=-1;const g=window.devicePixelRatio||1,B=i[0].rangeLow,X=i[i.length-1].rangeHigh,ot=X-B||1,Z=vt(Math.max(Math.abs(B),Math.abs(X)),O);function Y(p){const m=tt+36,e=h.clientWidth-m-tt,n=p-St-at;return{plotX:m,plotMaxWidth:Math.max(10,e),plotMaxHeight:n,plotTop:at}}function ct(p){const{plotX:m,plotMaxWidth:e}=Y(mt);if(p<m||p>m+e)return-1;const n=Math.floor((p-m)/e*i.length);return n>=0&&n<i.length?n:-1}function H(){Mt(h,P.offsetHeight);const p=u.clientWidth,m=yt(b,u,h,mt);h.width=Math.max(1,Math.floor(p*g)),h.height=Math.max(1,Math.floor(m*g)),h.style.width=`${p}px`,h.style.height=`${m}px`;const e=h.getContext("2d");e.setTransform(g,0,0,g,0,0),e.clearRect(0,0,p,m),e.font="11px system-ui, sans-serif";const n=Tt(b),{plotX:c,plotMaxWidth:t,plotMaxHeight:d,plotTop:o}=Y(m),l=o+d,S=t/i.length;kt(e,c,o,l,t,D,{text:n.textMuted,axis:n.border,grid:n.border}),i.forEach((M,C)=>{const k=M.count/D*d,r=c+C*S,L=l-k;e.fillStyle=C===v?n.text:Wt,e.globalAlpha=C===v?1:.7,e.fillRect(r+1,L,Math.max(1,S-2),k),e.globalAlpha=1,C===v&&(e.fillStyle=n.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${M.count}`,r+S/2,L-2))});const V=M=>c+(M-B)/ot*t;e.font="10px system-ui, sans-serif";for(const[M,C]of[[nt,"LSL"],[x,"USL"]]){if(M===void 0)continue;const k=V(M);e.strokeStyle=n.warnBorder,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(k,o),e.lineTo(k,l),e.stroke(),e.setLineDash([]),e.fillStyle=n.warnBorder,e.textAlign="center",e.textBaseline="bottom",e.fillText(C,k,o-2)}e.font="11px system-ui, sans-serif",e.strokeStyle=n.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,l),e.lineTo(c+t,l),e.stroke(),e.fillStyle=n.textMuted,e.textAlign="center",e.textBaseline="top";const W=Math.max(2,Math.floor(t/44)),dt=Math.ceil(i.length/W),ht=Math.max(1,dt);for(let M=0;M<=i.length;M+=ht){const C=M<i.length?i[M].rangeLow:i[i.length-1].rangeHigh,k=c+M*S;e.beginPath(),e.moveTo(k,l),e.lineTo(k,l+4),e.stroke(),e.fillText(Z.tick(C),k,l+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})`,c+t,l+20),e.restore())}h.addEventListener("mousemove",p=>{const m=h.getBoundingClientRect(),e=ct(p.clientX-m.left);if(e!==v&&(v=e,H()),e>=0){const n=i[e];A.innerHTML=`<strong>${st(n.rangeLow,O,"engineering")} \u2013 ${st(n.rangeHigh,O,"engineering")}</strong><br>${n.count} dies`,A.style.display="block",bt(A,b,p.clientX,p.clientY)}else A.style.display="none"}),h.addEventListener("mouseleave",()=>{v!==-1&&(v=-1,H()),A.style.display="none"}),J=xt(b,()=>H()),H()}function Lt(a){const{unit:w,limitLow:O,limitHigh:nt}=et(f),x=a.ranges,i=a.series,D=Ct;y&&!i.some(n=>n.groupKey===y)&&(y=null);const P=Math.max(1,...i.flatMap(n=>n.counts)),h=document.createElement("div");Object.assign(h.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),h.textContent=`${i.length} groups \xB7 max ${P} dies/bucket`,u.appendChild(h);const v=document.createElement("div");Object.assign(v.style,{display:"flex",flexWrap:"wrap",gap:"4px 12px",marginBottom:"4px"}),i.forEach((n,c)=>{const t=document.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 d=document.createElement("span");Object.assign(d.style,{width:"10px",height:"10px",borderRadius:"2px",background:D(c),flex:"0 0 auto"});const o=document.createElement("span");o.textContent=n.groupKey,t.append(d,o);const l=y!==null&&y!==n.groupKey;t.style.opacity=l?"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()}),v.appendChild(t)}),u.appendChild(v);const g=document.createElement("canvas");g.style.display="block",g.style.cursor="default",u.appendChild(g);const B=window.devicePixelRatio||1,X=x[0].rangeLow,ot=x[x.length-1].rangeHigh,Z=ot-X||1,Y=vt(Math.max(Math.abs(X),Math.abs(ot)),w),ct=()=>h.offsetHeight+v.offsetHeight;let H=-1,p={plotX:tt+36,plotMaxWidth:10,barWidth:10};function m(){Mt(g,ct());const n=u.clientWidth,c=yt(b,u,g,mt);g.width=Math.max(1,Math.floor(n*B)),g.height=Math.max(1,Math.floor(c*B)),g.style.width=`${n}px`,g.style.height=`${c}px`;const t=g.getContext("2d");t.setTransform(B,0,0,B,0,0),t.clearRect(0,0,n,c),t.font="11px system-ui, sans-serif";const d=Tt(b),o=tt+36,l=Math.max(10,n-o-tt),S=at,V=c-St-at,T=S+V,W=l/x.length;p={plotX:o,plotMaxWidth:l,barWidth:W},kt(t,o,S,T,l,P,{text:d.textMuted,axis:d.border,grid:d.border}),H>=0&&(t.fillStyle=d.bgHover,t.globalAlpha=.5,t.fillRect(o+H*W,S,W,V),t.globalAlpha=1);const dt=i.map((r,L)=>({s:r,i:L})).sort((r,L)=>(r.s.groupKey===y?1:0)-(L.s.groupKey===y?1:0));for(const{s:r,i:L}of dt){const E=y===r.groupKey,ut=y!==null&&!E,ft=D(L);t.strokeStyle=ft,t.fillStyle=ft,t.lineWidth=E?2:1,t.globalAlpha=ut?.12:y?.32:.22,t.beginPath(),t.moveTo(o,T),r.counts.forEach((gt,it)=>{const Q=o+it*W,q=T-gt/P*V;t.lineTo(Q,q),t.lineTo(Q+W,q)}),t.lineTo(o+l,T),t.closePath(),t.fill(),t.globalAlpha=ut?.3:1,t.beginPath(),r.counts.forEach((gt,it)=>{const Q=o+it*W,q=T-gt/P*V;it===0?t.moveTo(Q,q):t.lineTo(Q,q),t.lineTo(Q+W,q)}),t.stroke()}t.globalAlpha=1;const ht=r=>o+(r-X)/Z*l;t.font="10px system-ui, sans-serif";for(const[r,L]of[[O,"LSL"],[nt,"USL"]]){if(r===void 0)continue;const E=ht(r);t.strokeStyle=d.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(E,S),t.lineTo(E,T),t.stroke(),t.setLineDash([]),t.fillStyle=d.warnBorder,t.textAlign="center",t.textBaseline="bottom",t.fillText(L,E,S-2)}t.font="11px system-ui, sans-serif",t.strokeStyle=d.border,t.lineWidth=1,t.beginPath(),t.moveTo(o,T),t.lineTo(o+l,T),t.stroke(),t.fillStyle=d.textMuted,t.textAlign="center",t.textBaseline="top";const C=Math.max(2,Math.floor(l/44)),k=Math.max(1,Math.ceil(x.length/C));for(let r=0;r<=x.length;r+=k){const L=r<x.length?x[r].rangeLow:x[x.length-1].rangeHigh,E=o+r*W;t.beginPath(),t.moveTo(E,T),t.lineTo(E,T+4),t.stroke(),t.fillText(Y.tick(L),E,T+6)}Y.unitLabel&&(t.save(),t.font="10px system-ui, sans-serif",t.fillStyle=d.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${Y.unitLabel})`,o+l,T+20),t.restore())}function e(n){const{plotX:c,plotMaxWidth:t,barWidth:d}=p;if(n<c||n>c+t)return-1;const o=Math.floor((n-c)/d);return o>=0&&o<x.length?o:-1}g.addEventListener("mousemove",n=>{const c=g.getBoundingClientRect(),t=e(n.clientX-c.left);if(t!==H&&(H=t,m()),t>=0){const d=x[t],o=i.map((l,S)=>`<span style="display:inline-block;width:9px;height:9px;border-radius:2px;background:${D(S)};margin-right:5px;"></span>${l.groupKey}: ${l.counts[t]}`).join("<br>");A.innerHTML=`<strong>${st(d.rangeLow,w,"engineering")} \u2013 ${st(d.rangeHigh,w,"engineering")}</strong><br>${o}`,A.style.display="block",bt(A,b,n.clientX,n.clientY)}else A.style.display="none"}),g.addEventListener("mouseleave",()=>{H!==-1&&(H=-1,m()),A.style.display="none"}),J=xt(b,()=>m()),m()}U();function wt(a){!F.some(w=>w.testNumber===a)||a===f||(f=a,K.value=String(a),U())}return{card:b,setTest:wt,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 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()}}
@@ -8,6 +8,11 @@ export interface RegionYieldDiagramOptions {
8
8
  rows: RegionYieldDatum[];
9
9
  colorScheme?: string;
10
10
  onSaveImage?: SaveImageHandler;
11
+ /** Document to build this panel's DOM into. Default `document` — pass the
12
+ * host's own `ownerDocument` when the container might live in a
13
+ * different document (e.g. a gallery card detached into its own popup
14
+ * window). */
15
+ ownerDocument?: Document;
11
16
  }
12
17
  export interface RegionYieldDiagramHandle {
13
18
  card: HTMLElement;
@@ -1 +1 @@
1
- import{parseRegionKey as W}from"../../stats/regions.js";import{yieldFill as E}from"./palette.js";import{cardShell as A,resolveChartCanvasColors as N,observeResize as F,growCardToFitContent as D,renderEmptyState as $}from"./chartShell.js";const k=320,j=220,z={SE:[0,Math.PI/2],SW:[Math.PI/2,Math.PI],NW:[Math.PI,1.5*Math.PI],NE:[1.5*Math.PI,2*Math.PI]};function O(t,o,n,d,c,a){t.beginPath(),t.moveTo(o+a,n),t.arcTo(o+d,n,o+d,n+c,a),t.arcTo(o+d,n+c,o,n+c,a),t.arcTo(o,n+c,o,n,a),t.arcTo(o,n,o+d,n,a),t.closePath()}function w(t,o,n,d,c,a){t.font="10px system-ui, sans-serif";const g=t.measureText(d).width;t.font="700 11px system-ui, sans-serif";const b=t.measureText(c).width,l=Math.max(g,b)+12,M=30;t.fillStyle=a.bg,O(t,o-l/2,n-M/2,l,M,4),t.fill(),t.strokeStyle=a.border,t.lineWidth=1,t.stroke(),t.textAlign="center",t.textBaseline="middle",t.fillStyle=a.textMuted,t.font="10px system-ui, sans-serif",t.fillText(d,o,n-7),t.fillStyle=a.text,t.font="700 11px system-ui, sans-serif",t.fillText(c,o,n+7)}export function renderRegionYieldDiagram(t){const{mode:o,rows:n,onSaveImage:d}=t,c=t.title??(o==="ring"?"Ring yield":"Quadrant yield"),{card:a,body:g}=A(c,d),b=n.length>0;if(!b)return $(g,"No bin data available for regional yield."),{card:a,hasData:b,destroy:()=>{}};Object.assign(g.style,{display:"flex",justifyContent:"center",alignItems:"center"});const l=document.createElement("canvas");Object.assign(l.style,{display:"block",flex:"none"}),g.appendChild(l);const M=window.devicePixelRatio||1;function p(){const r=Math.max(j,Math.min(g.clientWidth||k,k));D(a,g,r),l.width=Math.max(1,Math.floor(r*M)),l.height=Math.max(1,Math.floor(r*M)),l.style.width=`${r}px`,l.style.height=`${r}px`;const e=l.getContext("2d");e.setTransform(M,0,0,M,0,0),e.clearRect(0,0,r,r);const u=N(a),T=E,h=r/2,f=r/2,m=r/2-34;if(o==="ring"){const i=n.length;for(let s=0;s<i;s++){const y=s/i*m,P=(s+1)/i*m;e.beginPath(),e.arc(h,f,P,0,2*Math.PI),e.arc(h,f,y,0,2*Math.PI,!0),e.closePath(),e.fillStyle=T(n[s].yieldPercent),e.fill(),e.strokeStyle=u.border,e.lineWidth=1,e.stroke()}e.beginPath(),e.arc(h,f,m,0,2*Math.PI),e.strokeStyle=u.text,e.lineWidth=1.5,e.stroke();for(let s=0;s<i;s++){const y=(s+.5)/i*m,P=-Math.PI/2+s/i*2*Math.PI,I=h+Math.cos(P)*y,S=f+Math.sin(P)*y;w(e,I,S,n[s].label,`${n[s].yieldPercent.toFixed(1)}%`,u)}}else{for(const i of n){const s=W(i.key).quadrant,y=s?z[s]:void 0;if(!y)continue;const[P,I]=y;e.beginPath(),e.moveTo(h,f),e.arc(h,f,m,P,I),e.closePath(),e.fillStyle=T(i.yieldPercent),e.fill(),e.strokeStyle=u.border,e.lineWidth=1,e.stroke();const S=(P+I)/2,R=h+Math.cos(S)*m*.6,C=f+Math.sin(S)*m*.6;w(e,R,C,i.label,`${i.yieldPercent.toFixed(1)}%`,u)}e.beginPath(),e.arc(h,f,m,0,2*Math.PI),e.strokeStyle=u.text,e.lineWidth=1.5,e.stroke()}}const v=F(a,()=>p());return p(),{card:a,hasData:b,destroy:()=>v.disconnect()}}
1
+ import{parseRegionKey as W}from"../../stats/regions.js";import{yieldFill as E}from"./palette.js";import{cardShell as A,resolveChartCanvasColors as D,observeResize as N,growCardToFitContent as F,renderEmptyState as $}from"./chartShell.js";const T=320,j=220,z={SE:[0,Math.PI/2],SW:[Math.PI/2,Math.PI],NW:[Math.PI,1.5*Math.PI],NE:[1.5*Math.PI,2*Math.PI]};function O(t,a,n,c,d,o){t.beginPath(),t.moveTo(a+o,n),t.arcTo(a+c,n,a+c,n+d,o),t.arcTo(a+c,n+d,a,n+d,o),t.arcTo(a,n+d,a,n,o),t.arcTo(a,n,a+c,n,o),t.closePath()}function k(t,a,n,c,d,o){t.font="10px system-ui, sans-serif";const g=t.measureText(c).width;t.font="700 11px system-ui, sans-serif";const b=t.measureText(d).width,l=Math.max(g,b)+12,M=30;t.fillStyle=o.bg,O(t,a-l/2,n-M/2,l,M,4),t.fill(),t.strokeStyle=o.border,t.lineWidth=1,t.stroke(),t.textAlign="center",t.textBaseline="middle",t.fillStyle=o.textMuted,t.font="10px system-ui, sans-serif",t.fillText(c,a,n-7),t.fillStyle=o.text,t.font="700 11px system-ui, sans-serif",t.fillText(d,a,n+7)}export function renderRegionYieldDiagram(t){const{mode:a,rows:n,onSaveImage:c}=t,d=t.title??(a==="ring"?"Ring yield":"Quadrant yield"),{card:o,body:g}=A(d,c,t.ownerDocument),b=n.length>0;if(!b)return $(g,"No bin data available for regional yield."),{card:o,hasData:b,destroy:()=>{}};Object.assign(g.style,{display:"flex",justifyContent:"center",alignItems:"center"});const l=o.ownerDocument.createElement("canvas");Object.assign(l.style,{display:"block",flex:"none"}),g.appendChild(l);const M=window.devicePixelRatio||1;function p(){const r=Math.max(j,Math.min(g.clientWidth||T,T));F(o,g,r),l.width=Math.max(1,Math.floor(r*M)),l.height=Math.max(1,Math.floor(r*M)),l.style.width=`${r}px`,l.style.height=`${r}px`;const e=l.getContext("2d");e.setTransform(M,0,0,M,0,0),e.clearRect(0,0,r,r);const u=D(o),w=E,h=r/2,f=r/2,m=r/2-34;if(a==="ring"){const i=n.length;for(let s=0;s<i;s++){const y=s/i*m,P=(s+1)/i*m;e.beginPath(),e.arc(h,f,P,0,2*Math.PI),e.arc(h,f,y,0,2*Math.PI,!0),e.closePath(),e.fillStyle=w(n[s].yieldPercent),e.fill(),e.strokeStyle=u.border,e.lineWidth=1,e.stroke()}e.beginPath(),e.arc(h,f,m,0,2*Math.PI),e.strokeStyle=u.text,e.lineWidth=1.5,e.stroke();for(let s=0;s<i;s++){const y=(s+.5)/i*m,P=-Math.PI/2+s/i*2*Math.PI,I=h+Math.cos(P)*y,S=f+Math.sin(P)*y;k(e,I,S,n[s].label,`${n[s].yieldPercent.toFixed(1)}%`,u)}}else{for(const i of n){const s=W(i.key).quadrant,y=s?z[s]:void 0;if(!y)continue;const[P,I]=y;e.beginPath(),e.moveTo(h,f),e.arc(h,f,m,P,I),e.closePath(),e.fillStyle=w(i.yieldPercent),e.fill(),e.strokeStyle=u.border,e.lineWidth=1,e.stroke();const S=(P+I)/2,R=h+Math.cos(S)*m*.6,C=f+Math.sin(S)*m*.6;k(e,R,C,i.label,`${i.yieldPercent.toFixed(1)}%`,u)}e.beginPath(),e.arc(h,f,m,0,2*Math.PI),e.strokeStyle=u.text,e.lineWidth=1.5,e.stroke()}}const v=N(o,()=>p());return p(),{card:o,hasData:b,destroy:()=>v.disconnect()}}
@@ -25,6 +25,11 @@ export interface ScatterPanelOptions {
25
25
  key: string;
26
26
  items: ScatterPanelItem[];
27
27
  }[];
28
+ /** Document to build this panel's DOM into. Default `document` — pass the
29
+ * host's own `ownerDocument` when the container might live in a
30
+ * different document (e.g. a gallery card detached into its own popup
31
+ * window). */
32
+ ownerDocument?: Document;
28
33
  }
29
34
  export interface ScatterPanelHandle {
30
35
  card: HTMLElement;