@wafertools/wafermap 0.24.2 → 0.25.0

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