@wafertools/wafermap 0.24.3 → 0.26.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 (34) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  3. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +16 -0
  4. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/correlation.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  8. package/dist/packages/canvas-adapter/dieList.d.ts +17 -0
  9. package/dist/packages/canvas-adapter/dieList.js +7 -7
  10. package/dist/packages/canvas-adapter/icons.js +1 -1
  11. package/dist/packages/canvas-adapter/index.d.ts +4 -0
  12. package/dist/packages/canvas-adapter/index.js +1 -1
  13. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +14 -1
  14. package/dist/packages/canvas-adapter/renderWaferGallery.js +6 -6
  15. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +22 -0
  16. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  17. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  18. package/dist/packages/canvas-adapter/toCanvas.d.ts +11 -0
  19. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  20. package/dist/packages/canvas-adapter/toolbar.d.ts +108 -6
  21. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  22. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  23. package/dist/packages/canvas-adapter/userGuideHtml.js +42 -4
  24. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  25. package/dist/packages/canvas-adapter/version.js +1 -1
  26. package/dist/packages/renderer/buildView.d.ts +22 -0
  27. package/dist/packages/renderer/buildView.js +1 -1
  28. package/dist/packages/renderer/buildWaferMap.d.ts +5 -1
  29. package/dist/packages/renderer/buildWaferMap.js +1 -1
  30. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  31. package/dist/packages/stats/renderSummaryReport.js +1 -1
  32. package/dist/packages/stats/reportHtml.d.ts +6 -4
  33. package/dist/packages/stats/types.d.ts +14 -1
  34. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -22,6 +22,65 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
25
+ ## [0.26.0] — 2026-08-28
26
+
27
+ ### Breaking
28
+
29
+ - **The die-list table/CSV's single `Position` column (`"(x, y)"`) is now two separate `X`/`Y` columns.** A bracketed pair reads fine on screen but forces an extra parsing step (or breaks outright) when the CSV is opened in Excel, pandas, or any other tool expecting one number per cell. Any code reading the die-list CSV/table by column name or position needs to read `X`/`Y` instead of `Position`.
30
+
31
+ ### Added
32
+
33
+ - **`Ring`/`Quadrant` die-list columns**, via new `DieListOptions.getWafer`/`ringCount`. Wired up automatically through `renderWaferMap`/`renderWaferGallery` — a direct `buildDieListSection` call supplies `getWafer` itself. Appear only when `getWafer` resolves at least one die to a `Wafer` (omitted entirely, not shown empty, otherwise), and use the same `classifyDie`/`ringCount` the rest of that wafer/lot's analysis uses, so "Ring 2" here always names the same region a Ring finding does.
34
+ - **`Edge excluded` die-list column**, shown only when at least one die in the export has `edgeExcluded: true` (an included die is never stamped `false`, so "no die is excluded" and "the feature was never configured" are indistinguishable from `Die[]` alone — omitting the column in that case is the closest available approximation).
35
+ - **`openReportModal`** (`/render`) — opens report HTML (from `renderFindingsReportHtml`/`renderSummaryReportHtml`/`renderLotSummaryReportHtml`) in an in-app modal, with a Print/Save-as-PDF header button and an "Open as full page ↗" fallback to `openHtmlReport`/`setReportOpener`. The Summary panel's "Summary report" button now calls this automatically — **no `setReportOpener` wiring is required just to view a report anymore**, in a plain browser tab or an embedded host (Tauri, Electron, WebView2) alike.
36
+ - **`openWaferMapGuide`** (`/render`) — opens the built-in guide window with no live `WaferMapController`/`GalleryController` required, for a host whose help entry point must also work before anything has rendered (an empty-state "Help" menu). `WaferMapController.openUserGuide()`/`GalleryController.openUserGuide()` are now thin wrappers around this same call.
37
+ - **`ICONS`** (`/render`) — the toolbar's own icon set, now public so a host rendering its own chrome alongside wmap's can match its iconography instead of copy-pasting SVGs that drift on the next redesign.
38
+ - **Combined "Contents" navigation and Print/Save-as-PDF in the guide window.** A sticky bar lists every `<h2 id>` across the host's `extension.html` and wmap's own guide content as one flat, unnumbered list (see `UserGuideExtension`'s doc for why it's deliberately not split by source), flowing top-to-bottom in columns. A Print/Save-as-PDF header button covers the in-page overlay case (`window.open` blocked/unavailable); a real popup needs none, since native Ctrl+P already scopes to just that window.
39
+ - **Find-in-page search in the guide window** — but only for the in-page overlay case, next to the Contents toggle. A real popup window gets no search box: it's an actual OS window, so native Ctrl+F/Cmd+F already searches it, and a custom box there would just be redundant chrome. Matches highlight via plain `<mark>` elements (no invented highlight colour), Enter/Shift+Enter step through them, Escape clears.
40
+ - **`LotStatsSummary.mixedIdentityFields`** — identity keys (lot, product, testProgram, temperature, etc.) where the pooled wafers passed to `analyzeWaferLot` do *not* all agree, e.g. items pooled from more than one lot/program. `lot` now only includes a key when every wafer with identity data agrees on its value; a disagreeing key is omitted from `lot` and named here instead of silently taking the first wafer's value.
41
+ - **`WaferWarning` code `'edge-exclusion-exceeds-radius'`** (severity `'warning'`) — raised when `waferConfig.edgeExclusion` exceeds the resolved wafer radius (most likely with an under-inferred diameter); see the Fixed entry below for the behaviour this replaces.
42
+ - The gallery's toolbar and legend are now `position: sticky` in every one of the library's own demo pages, given a proper scrolling ancestor.
43
+
44
+ ### Fixed
45
+
46
+ - **CSV export was vulnerable to formula injection from untrusted die/wafer metadata.** A cell value beginning with `=`, `+`, `-`, or `@` is read as a formula by Excel/Sheets/LibreOffice on open — a real risk when the source is a host's own MES/LIMS metadata or operator free text, not data this library originated. `csvField` (shared by every CSV export, including the die list) now prefixes such a value with `'` before quoting — but only when it doesn't parse as a plain number, so a genuine negative/signed test value (offsets, leakage, deltas — routine in test data) round-trips unchanged.
47
+ - **An edge-exclusion width exceeding the resolved wafer radius silently produced a smaller, wrong excluded band** instead of excluding the whole wafer — squaring the resulting negative "inner radius" flipped its sign back positive, so the comparison it fed just picked the wrong dies. Now clamped to 0 and reported via the new `'edge-exclusion-exceeds-radius'` warning.
48
+ - **`analyzeWaferLot`'s lot identity (`LotStatsSummary.lot`) was silently derived from the first wafer only**, not verified shared — pooling wafers from two different lots/products into one call could report the first one's identity as if it applied to the whole batch. See `mixedIdentityFields` above.
49
+
50
+ ## [0.25.0] — 2026-08-25
51
+
52
+ ### Added
53
+
54
+ - `WaferViewOptions.showLegend` (`renderWaferMap`) — hide the legend/colorbar entirely, for a host that provides its own equivalent control. Default `true`, matching prior behaviour.
55
+ - `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`.
56
+ - `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.
57
+ - `ViewOptions.passBins` (`buildView`) / `ViewRect.binFail` — internal plumbing for the failing-die marker above, relevant only to a caller using the low-level `buildView` directly (bypassing `renderWaferMap`/`renderWaferGallery`). It is **not** a new option for the normal `renderWaferMap`/`renderWaferGallery` path: both already required `passBins` before this release (for the summary panel's yield figure), and that existing value now also flows down into `buildView` automatically for the marker — nothing new to set, no second place to keep in sync. A direct `buildView` caller does need to pass it explicitly, matching whatever they already pass to `buildWaferMap`/`analyzeWaferMap`, or the marker will disagree with the yield figure beside it. 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.
58
+
59
+ ### Fixed
60
+
61
+ - **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.
62
+ - **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.
63
+ - **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.
64
+ - **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".
65
+ - **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.
66
+ - **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.
67
+ - **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.
68
+ - **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.
69
+ - **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.
70
+ - **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.
71
+ - 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.
72
+ - 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.
73
+
74
+ ### Changed
75
+
76
+ - 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.
77
+
78
+ ### Internal
79
+
80
+ - `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.
81
+ - 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.
82
+ - **`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.
83
+
25
84
  ## [0.24.3] — 2026-08-25
26
85
 
27
86
  ### Fixed
@@ -1 +1 @@
1
- import{buildCapabilityData as ct}from"../../stats/capability.js";import{capabilityColor as mt}from"./palette.js";import{CLR as V}from"../toolbar.js";import{cardShell as pt,chartFillHeight as dt,applyCanvasFlow as ht,observeResize as ut,makeTooltip as ft,positionChartTooltip as yt,makeLabeledSelect as bt,renderEmptyState as xt,resolveChartCanvasColors as vt}from"./chartShell.js";import{fmt as gt}from"../../renderer/fmt.js";const Mt=30,kt=160,Y=90,J=12;function A(M){return M===null?"\u2014":M.toFixed(2)}export function renderCapabilityPanel(M){const{title:K="Process capability",items:Z,testDefs:E,onSaveImage:tt,onSelectTest:W,groups:d}=M,{card:s,body:h,controlsRow:et}=pt(K,tt,M.ownerDocument),nt=E.some(e=>e.testNumber!==void 0);h.style.overflowX="auto";let H=d&&d.length>0?d[0].key:void 0;d&&d.length>0&&et.appendChild(bt("Group:",d.map(e=>({value:e.key,label:e.key})),H??"",e=>{H=e,O()},{ownerDocument:s.ownerDocument}));function ot(){return!d||d.length===0?Z:d.find(e=>e.key===H)?.items??[]}const R=s.ownerDocument.createElement("div");Object.assign(R.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),s.insertBefore(R,h);const k=window.devicePixelRatio||1,S=ft(s);let B=()=>{};function lt(e,n,u){R.innerHTML="";const m=s.ownerDocument.createElement("span");Object.assign(m.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:V.value,fontSize:"12px",fontWeight:"500"});const C=u-e,T=n>0?` \xB7 ${n} without spec limits`:"",L=s.ownerDocument.createElement("span");L.textContent=C>0?`${e} of ${u} tests shown${T} \xB7 ${C} excluded (no recorded values)`:`${e} test${e!==1?"s":""} shown${T}`,m.appendChild(L);const $=s.ownerDocument.createElement("span");$.textContent="\u24D8",$.title="Spec-limited tests are normalized so LSL=0 and USL=1, sorted worst Ppk first. Tests without both spec limits (muted, dashed) are normalized to their own observed range, sorted most-variable first. Box colour: green Ppk \u2265 1.33 (capable), orange Ppk \u2265 1.0 (marginal), red Ppk < 1.0 (poor).",Object.assign($.style,{color:V.label,cursor:"help",fontSize:"12px"}),m.appendChild($),R.appendChild(m)}function it(e){if(h.innerHTML="",e.length===0)return xt(h,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const n=s.ownerDocument.createElement("canvas");n.style.display="block",n.style.cursor="default",h.appendChild(n);const u=e.length,m=Math.min(0,...e.map(a=>a.min)),C=Math.max(1,...e.map(a=>a.max)),T=(C-m)*.05||.1,L=m-T,st=C+T-L||1;function X(a){return Math.max(Mt,Math.min(kt,Math.floor(a/u)))}function b(a,o,l){return o+(1-(a-L)/st)*l}let w=-1;function N(){ht(n);const a=h.clientWidth,o=X(a),l=o*u,i=J,x=l,f=dt(s,h,n,J+200+Y),_=Math.max(i+60,f-Y),y=_-i;n.width=Math.max(1,Math.floor(x*k)),n.height=Math.max(1,Math.floor(f*k)),n.style.width=`${x}px`,n.style.height=`${f}px`;const t=n.getContext("2d");t.setTransform(k,0,0,k,0,0),t.clearRect(0,0,x,f);const v=vt(s);e.forEach((c,j)=>{const g=j*o,r=g+o/2,p=Math.max(4,o*.55);if(j===w&&(t.fillStyle=v.bgHover,t.fillRect(g,i,o,_-i)),c.hasSpec){const q=b(0,i,y),Q=b(1,i,y);t.strokeStyle=v.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(g,q),t.lineTo(g+o,q),t.moveTo(g,Q),t.lineTo(g+o,Q),t.stroke(),t.setLineDash([])}const G=c.hasSpec?mt(c.ppk):v.textMuted,z=b(c.min,i,y),P=b(c.q1,i,y),U=b(c.median,i,y),D=b(c.q3,i,y),I=b(c.max,i,y);t.strokeStyle=v.textMuted,t.lineWidth=1,t.beginPath(),t.moveTo(r,z),t.lineTo(r,P),t.moveTo(r,D),t.lineTo(r,I),t.moveTo(r-p/4,z),t.lineTo(r+p/4,z),t.moveTo(r-p/4,I),t.lineTo(r+p/4,I),t.stroke(),t.fillStyle=G,t.globalAlpha=.5,t.fillRect(r-p/2,Math.min(P,D),p,Math.max(1,Math.abs(D-P))),t.globalAlpha=1,t.strokeStyle=G,c.hasSpec||t.setLineDash([4,2]),t.strokeRect(r-p/2,Math.min(P,D),p,Math.max(1,Math.abs(D-P))),t.setLineDash([]),t.strokeStyle=v.text,t.lineWidth=2,t.beginPath(),t.moveTo(r-p/2,U),t.lineTo(r+p/2,U),t.stroke(),t.lineWidth=1;const rt=c.label.length>12?`${c.label.slice(0,11)}\u2026`:c.label;t.save(),t.translate(r,_+6),t.rotate(-Math.PI/4),t.textAlign="right",t.textBaseline="middle",t.font="10px system-ui, sans-serif",t.fillStyle=v.textMuted,t.fillText(rt,0,0),t.restore()}),t.strokeStyle=v.border,t.lineWidth=1,t.strokeRect(0,i,l,y)}function F(a){const o=n.getBoundingClientRect(),l=h.clientWidth,i=X(l),x=(a.clientX-o.left)*(n.width/k/o.width),f=Math.floor(x/i);return f>=0&&f<u?f:-1}return n.addEventListener("mousemove",a=>{const o=F(a);if(o!==w&&(w=o,N()),o===-1){S.style.display="none";return}const l=e[o],i=x=>gt(x,l.unit);S.innerHTML=`<strong>${l.label}</strong> (n=${l.n})<br>`+(l.hasSpec?`LSL ${i(l.lsl)} \xB7 USL ${i(l.usl)}<br>mean ${i(l.mean)}<br>Cp ${A(l.cp)} \xB7 Cpk ${A(l.cpk)}<br>Pp ${A(l.pp)} \xB7 Ppk ${A(l.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${i(l.mean)} \xB7 stddev ${i(l.stdOverall)}`)+(W?"<br><em>click to view in boxplot</em>":""),S.style.display="block",yt(S,s,a.clientX,a.clientY),n.style.cursor=W?"pointer":"default"}),n.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,N()),S.style.display="none"}),n.addEventListener("click",a=>{if(!W)return;const o=F(a);o!==-1&&W(e[o].testNumber)}),N}function O(){const e=ct(ot(),E),n=E.filter(m=>m.testNumber!==void 0).length,u=e.filter(m=>!m.hasSpec).length;lt(e.length,u,n),B=it(e),B()}const at=ut(s,()=>B());return O(),{card:s,hasData:nt,destroy:()=>at.disconnect()}}
1
+ import{buildCapabilityData as he}from"../../stats/capability.js";import{capabilityColor as O}from"./palette.js";import{CLR as _}from"../toolbar.js";import{cardShell as fe,chartFillHeight as ue,applyCanvasFlow as be,observeResize as xe,makeTooltip as ye,positionChartTooltip as ge,makeLabeledSelect as ve,renderEmptyState as Me,resolveChartCanvasColors as Se}from"./chartShell.js";import{fmt as ke}from"../../renderer/fmt.js";const Te=30,Le=160,Z=90,ee=28,v=42;function I($){return $===null?"\u2014":$.toFixed(2)}export function renderCapabilityPanel($){const{title:te="Process capability",items:ne,testDefs:H,onSaveImage:oe,onSelectTest:B,groups:x}=$,{card:c,body:M,controlsRow:le}=fe(te,oe,$.ownerDocument),ie=H.some(n=>n.testNumber!==void 0);M.style.overflowX="auto";let j=x&&x.length>0?x[0].key:void 0;x&&x.length>0&&le.appendChild(ve("Group:",x.map(n=>({value:n.key,label:n.key})),j??"",n=>{j=n,q()},{ownerDocument:c.ownerDocument}));function se(){return!x||x.length===0?ne:x.find(n=>n.key===j)?.items??[]}const C=c.ownerDocument.createElement("div");Object.assign(C.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),c.insertBefore(C,M);const W=window.devicePixelRatio||1,A=ye(c);let z=()=>{};function ae(n,i,u){C.innerHTML="";const d=c.ownerDocument.createElement("span");Object.assign(d.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:_.value,fontSize:"12px",fontWeight:"500"});const S=u-n,m=i>0?` \xB7 ${i} without spec limits`:"",y=c.ownerDocument.createElement("span");y.textContent=S>0?`${n} of ${u} tests shown${m} \xB7 ${S} excluded (no recorded values)`:`${n} test${n!==1?"s":""} shown${m}`,d.appendChild(y),C.appendChild(d);const w=i>=n,k=c.ownerDocument.createElement("div");k.textContent=w?"No test has both spec limits, so each is normalised to its own observed range (min = 0, max = 1) and sorted most-variable first. Ppk needs limits, so none is shown.":"Normalised to spec limits (LSL = 0, USL = 1), worst Ppk first. Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(k.style,{color:_.label,fontSize:"11px",lineHeight:"1.45",marginTop:"3px",maxWidth:"78ch"}),C.appendChild(k),C.appendChild(re(c.ownerDocument,i>0,w))}function re(n,i,u=!1){const d=n.createElement("div");Object.assign(d.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:"4px 14px",marginTop:"7px",fontSize:"11px",color:_.label});const S=u?[]:[{color:O(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:O(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:O(.5),label:"Poor \xB7 Ppk < 1.0"}];i&&S.push({color:_.label,label:"No spec limits \xB7 no Ppk",dashed:!0});for(const m of S){const y=n.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:"5px"});const w=n.createElement("span");Object.assign(w.style,{width:"11px",height:"11px",flexShrink:"0",borderRadius:"2px",background:m.dashed?"transparent":m.color,border:`1px ${m.dashed?"dashed":"solid"} ${m.color}`,opacity:m.dashed?"1":"0.75"});const k=n.createElement("span");k.textContent=m.label,y.append(w,k),d.appendChild(y)}return d}function ce(n){if(M.innerHTML="",n.length===0)return Me(M,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const i=c.ownerDocument.createElement("canvas");i.style.display="block",i.style.cursor="default",M.appendChild(i);const u=n.length,d=Math.min(0,...n.map(t=>t.min)),S=Math.max(1,...n.map(t=>t.max)),m=(S-d)*.05||.1,y=d-m,k=S+m-y||1;function Q(t){return Math.max(Te,Math.min(Le,Math.floor(t/u)))}function T(t,s,o){return s+(1-(t-y)/k)*o}function pe(t,s,o,l,g){const h=n.filter(P=>P.hasSpec).length,E=h===n.length,p=h===0,e=p?"max":"USL",f=p?"min":"LSL",r=E?"normalised to spec":p?"normalised to range":"normalised (per test)";t.font="10px system-ui, sans-serif",t.textAlign="right",t.textBaseline="middle";for(const[P,L]of[[1,e],[0,f]]){const a=T(P,o,l);a<o-1||a>o+l+1||(t.strokeStyle=s.warnBorder,t.lineWidth=1,t.beginPath(),t.moveTo(v-5,a),t.lineTo(v,a),t.stroke(),t.fillStyle=s.textMuted,t.fillText(L,v-8,a))}t.save(),t.translate(9,o+l/2),t.rotate(-Math.PI/2),t.textAlign="center",t.fillStyle=s.textMuted,t.fillText(r,0,0),t.restore()}let D=-1;function X(){be(i);const t=M.clientWidth,s=Q(t-v),o=s*u,l=ee,g=v+o,h=ue(c,M,i,ee+200+Z),E=Math.max(l+60,h-Z),p=E-l;i.width=Math.max(1,Math.floor(g*W)),i.height=Math.max(1,Math.floor(h*W)),i.style.width=`${g}px`,i.style.height=`${h}px`;const e=i.getContext("2d");e.setTransform(W,0,0,W,0,0),e.clearRect(0,0,g,h);const f=Se(c);n.forEach((r,P)=>{const L=v+P*s,a=L+s/2,b=Math.max(4,s*.55);if(P===D&&(e.fillStyle=f.bgHover,e.fillRect(L,l,s,E-l)),r.hasSpec){const J=T(0,l,p),K=T(1,l,p);e.strokeStyle=f.warnBorder,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(L,J),e.lineTo(L+s,J),e.moveTo(L,K),e.lineTo(L+s,K),e.stroke(),e.setLineDash([])}const U=r.hasSpec?O(r.ppk):f.textMuted,F=T(r.min,l,p),R=T(r.q1,l,p),Y=T(r.median,l,p),N=T(r.q3,l,p),G=T(r.max,l,p);e.strokeStyle=f.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(a,F),e.lineTo(a,R),e.moveTo(a,N),e.lineTo(a,G),e.moveTo(a-b/4,F),e.lineTo(a+b/4,F),e.moveTo(a-b/4,G),e.lineTo(a+b/4,G),e.stroke(),e.fillStyle=U,e.globalAlpha=.5,e.fillRect(a-b/2,Math.min(R,N),b,Math.max(1,Math.abs(N-R))),e.globalAlpha=1,e.strokeStyle=U,r.hasSpec||e.setLineDash([4,2]),e.strokeRect(a-b/2,Math.min(R,N),b,Math.max(1,Math.abs(N-R))),e.setLineDash([]),e.strokeStyle=f.text,e.lineWidth=2,e.beginPath(),e.moveTo(a-b/2,Y),e.lineTo(a+b/2,Y),e.stroke(),e.lineWidth=1,e.font="10px system-ui, sans-serif",e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=r.hasSpec?U:f.textMuted,e.fillText(r.hasSpec?I(r.ppk):"\u2014",a,l-8);const me=r.label.length>12?`${r.label.slice(0,11)}\u2026`:r.label;e.save(),e.translate(a,E+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font="10px system-ui, sans-serif",e.fillStyle=f.textMuted,e.fillText(me,0,0),e.restore()}),e.strokeStyle=f.border,e.lineWidth=1,e.strokeRect(v,l,o,p),pe(e,f,l,p,o)}function V(t){const s=i.getBoundingClientRect(),o=M.clientWidth,l=Q(o-v),g=(t.clientX-s.left)*(i.width/W/s.width)-v,h=Math.floor(g/l);return h>=0&&h<u?h:-1}return i.addEventListener("mousemove",t=>{const s=V(t);if(s!==D&&(D=s,X()),s===-1){A.style.display="none";return}const o=n[s],l=g=>ke(g,o.unit);A.innerHTML=`<strong>${o.label}</strong> (n=${o.n})<br>`+(o.hasSpec?`LSL ${l(o.lsl)} \xB7 USL ${l(o.usl)}<br>mean ${l(o.mean)}<br>Cp ${I(o.cp)} \xB7 Cpk ${I(o.cpk)}<br>Pp ${I(o.pp)} \xB7 Ppk ${I(o.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${l(o.mean)} \xB7 stddev ${l(o.stdOverall)}`)+(B?"<br><em>click to view in boxplot</em>":""),A.style.display="block",ge(A,c,t.clientX,t.clientY),i.style.cursor=B?"pointer":"default"}),i.addEventListener("mouseleave",()=>{D!==-1&&(D=-1,X()),A.style.display="none"}),i.addEventListener("click",t=>{if(!B)return;const s=V(t);s!==-1&&B(n[s].testNumber)}),X}function q(){const n=he(se(),H),i=H.filter(d=>d.testNumber!==void 0).length,u=n.filter(d=>!d.hasSpec).length;ae(n.length,u,i),z=ce(n),z()}const de=xe(c,()=>z());return q(),{card:c,hasData:ie,destroy:()=>de.disconnect()}}
@@ -149,6 +149,22 @@ export declare function makeLabeledSelect(labelText: string, options: readonly {
149
149
  export declare function makeChartGridWrap(ownerDocument?: Document): HTMLDivElement;
150
150
  /** Small positioned hover tooltip, matching the pattern each ported panel builds for itself. */
151
151
  export declare function makeTooltip(card: HTMLElement): HTMLElement;
152
+ /**
153
+ * Give a chart-card control the library's themed hover tooltip instead of a
154
+ * native `title`.
155
+ *
156
+ * toolbar.ts states the policy for the map toolbar — `ariaLabel` plus the
157
+ * custom tooltip, deliberately no `title`, because a native tooltip is slow,
158
+ * unthemeable, invisible on touch and unreachable by keyboard. The chart layer
159
+ * never adopted it and kept setting `title` directly, so hints that make an
160
+ * interaction discoverable ("click to filter") were reachable only by hovering
161
+ * with a mouse and knowing to hover in the first place.
162
+ *
163
+ * `ariaLabel` is set only when the element has no text of its own — on a
164
+ * labelled element it would override the visible name for screen readers,
165
+ * which is worse than the tooltip it replaces.
166
+ */
167
+ export declare function attachChartTip(el: HTMLElement, card: HTMLElement, tooltip: HTMLElement, text: string): void;
152
168
  /**
153
169
  * Position a `makeTooltip` element from a mousemove event, flipping to the
154
170
  * opposite side of the cursor when the default placement would overflow
@@ -1 +1 @@
1
- import{CLR as a,Z_BASE as T,MENU_SEARCH_THRESHOLD as N,makeMenuSearchBox as M,markMenuTrigger as E,saveImageBlob as I,openReparentedModal as z}from"../toolbar.js";import{ICONS as O}from"../icons.js";import{fmt as W,fmtColorbarAxis as F}from"../../renderer/fmt.js";export function openChartExpandModal(e,o,r){const t=e.getAttribute("style")??"";if(Object.assign(e.style,{flex:"1",minHeight:"0",border:"none",borderRadius:"0"}),!z([e],{title:o,onClosed:()=>{e.setAttribute("style",t),r&&(r.style.display="flex")}})){e.setAttribute("style",t);return}r&&(r.style.display="none")}const P={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040"},j={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border"};export function resolveChartCanvasColors(e){const o=getComputedStyle(e),r={};for(const t of Object.keys(j)){const i=o.getPropertyValue(`--wmap-${j[t]}`).trim();r[t]=i||P[t]}return r}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,o){const r=new ResizeObserver(o);return r.observe(e),{disconnect:()=>r.disconnect()}}export function growCardToFitContent(e,o,r){const t=e.offsetHeight-o.clientHeight;e.style.minHeight=`${t+r}px`}export function chartFillHeight(e,o,r,t){let i=0;for(const s of Array.from(o.children))s!==r&&(i+=s.offsetHeight);return growCardToFitContent(e,o,i+t),Math.max(t,o.clientHeight-i)}export function applyCanvasFlow(e,o=0){e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${o}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:W(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,o){if(!o)return{tick:formatValue,unitLabel:""};const{tickFmt:r,axisLabel:t}=F(e,null,o);return{tick:r,unitLabel:t}}export function drawAxisUnit(e,o,r,t,i){const s={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font="10px system-ui, sans-serif",e.textAlign="left",e.textBaseline="middle",e.fillStyle=i,e.fillText(`(${o})`,r,t),Object.assign(e,s)}export function saveCanvasPng(e,o,r){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const i=t.getContext("2d");i.fillStyle="#ffffff",i.fillRect(0,0,t.width,t.height),i.drawImage(e,0,0),t.toBlob(s=>{s&&I(s,o,r)},"image/png")}export function cardShell(e,o,r=document){const t=r.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"6px",padding:"12px",minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const i=t.ownerDocument.createElement("div");Object.assign(i.style,{display:"flex",alignItems:"center",gap:"6px",marginBottom:"6px"});const s=t.ownerDocument.createElement("div");s.textContent=e,Object.assign(s.style,{color:a.value,fontSize:"13px",fontWeight:"600",flex:"1"}),i.appendChild(s);const l=t.ownerDocument.createElement("button");l.type="button",l.title="Save as PNG",l.setAttribute("aria-label","Save as PNG"),l.innerHTML=O.download,Object.assign(l.style,{border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:"none",color:a.label,cursor:"pointer",width:"22px",height:"22px",lineHeight:"1",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),l.addEventListener("click",()=>{const u=t.querySelector("canvas");u&&saveCanvasPng(u,e,o)}),i.appendChild(l);const n=t.ownerDocument.createElement("button");n.type="button",n.title="Expand",n.setAttribute("aria-label","Expand"),n.innerHTML=O.expand,Object.assign(n.style,{border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:"none",color:a.label,cursor:"pointer",width:"22px",height:"22px",lineHeight:"1",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),n.addEventListener("click",()=>openChartExpandModal(t,s.textContent??e,n)),i.appendChild(n),t.appendChild(i);const d=t.ownerDocument.createElement("div");Object.assign(d.style,{display:"flex",gap:"6px",marginBottom:"6px",flexWrap:"wrap",alignItems:"center"}),t.appendChild(d);const p=t.ownerDocument.createElement("div");return Object.assign(p.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(p),{card:t,heading:s,controlsRow:d,body:p}}export function makeSegmented(e,o,r,t=document){const i=t.createElement("div");i.setAttribute("role","radiogroup"),Object.assign(i.style,{display:"inline-flex",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",overflow:"hidden"});const s=`seg-${Math.random().toString(36).slice(2,9)}`,l=[];return e.forEach(([n,d],p)=>{const u=t.createElement("label");Object.assign(u.style,{display:"inline-flex",alignItems:"center",fontSize:"12px",padding:"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:p>0?`1px solid ${a.menuBorder}`:"none"});const c=t.createElement("input");c.type="radio",c.name=s,c.value=n,c.checked=n===o,Object.assign(c.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const h=()=>{u.style.background=c.checked?a.bgActive:a.menuBg,u.style.color=c.checked?a.iconActive:a.text};l.push(h),h(),c.addEventListener("change",()=>{if(c.checked){for(const y of l)y();r(n)}}),u.append(c,t.createTextNode(d)),i.appendChild(u)}),i}export function makeBackButton(e,o=document){const r=o.createElement("button");return r.type="button",r.textContent="\u2190 Back",Object.assign(r.style,{fontSize:"12px",padding:"3px 10px",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:a.menuBg,color:a.label,cursor:"pointer"}),r.addEventListener("click",e),r}export function renderEmptyState(e,o,r){const t=e.ownerDocument.createElement("div");t.textContent=o,Object.assign(t.style,{color:a.label,fontSize:"12px",padding:"8px 0"}),r&&Object.assign(t.style,r),e.appendChild(t)}export function makeTestSelect(e,o,r,t={}){const{maxWidth:i="200px",emptyText:s="No parametric tests",ownerDocument:l=document}=t;if(e.length>N)return _(e,o,r,i,l);const n=l.createElement("select");if(Object.assign(n.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:i}),e.length===0){n.disabled=!0;const d=l.createElement("option");d.textContent=s,n.appendChild(d)}else{for(const d of e){const p=l.createElement("option");p.value=String(d.testNumber),p.textContent=d.name||`Test ${d.testNumber}`,d.testNumber===o&&(p.selected=!0),n.appendChild(p)}n.addEventListener("change",()=>r(Number(n.value)))}return n}function _(e,o,r,t,i=document){const s=x=>e.find(v=>v.testNumber===x)?.name||`Test ${x}`;let l=o;const n=i.createElement("button");n.type="button",Object.assign(n.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:t,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:"6px",justifyContent:"space-between"}),E(n,!1);const d=n.ownerDocument.createElement("span");Object.assign(d.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const p=n.ownerDocument.createElement("span");p.textContent="\u25BE",p.style.flex="0 0 auto",n.append(d,p);const u=()=>{d.textContent=l!==null?s(l):"(none)"};u();let c=null;const h=()=>{c&&(c.remove(),c=null,E(n,!1))},y=()=>{h(),n.focus()};function D(){const x=n.getBoundingClientRect(),v=n.ownerDocument.defaultView??window,S=Math.max(x.width,220),R=320,$=Math.min(x.left,Math.max(4,(v.innerWidth??1/0)-S-4));c=n.ownerDocument.createElement("div"),Object.assign(c.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${$}px`,background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",boxShadow:"0 4px 12px rgba(0,0,0,0.15)",zIndex:T,minWidth:`${S}px`,maxHeight:`${R}px`,overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const w=[],B=()=>w.filter(f=>f.row.style.display!=="none").map(f=>f.row),k=M(f=>{for(const m of w)m.row.style.display=m.label.includes(f)?"":"none"},"Filter tests\u2026",n.ownerDocument);Object.assign(k.style,{position:"sticky",top:"0",zIndex:"1",background:a.menuBg}),k.addEventListener("keydown",f=>{if(f.key==="Escape"){y();return}f.key==="ArrowDown"&&(f.preventDefault(),B()[0]?.focus())}),c.appendChild(k);for(const f of e){const m=f.name||`Test ${f.testNumber}`,b=f.testNumber===l,g=n.ownerDocument.createElement("div");g.textContent=m,g.setAttribute("role","menuitemradio"),g.setAttribute("aria-checked",b?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:"6px 14px",fontSize:"12px",cursor:"pointer",color:b?a.iconActive:a.text,fontWeight:b?"700":"400",background:b?a.menuActive:"transparent",whiteSpace:"nowrap",outline:"none"});const A=()=>{b||(g.style.background=a.menuHover)},L=()=>{g.style.background=b?a.menuActive:"transparent"};g.addEventListener("mouseenter",A),g.addEventListener("mouseleave",L),g.addEventListener("focus",A),g.addEventListener("blur",L),g.addEventListener("click",H=>{H.stopPropagation(),l=f.testNumber,u(),y(),r(f.testNumber)}),c.appendChild(g),w.push({row:g,label:m.toLowerCase()})}c.addEventListener("keydown",f=>{const m=B();if(m.length===0)return;const b=m.indexOf(n.ownerDocument.activeElement);switch(f.key){case"ArrowDown":f.preventDefault(),m[b<0||b===m.length-1?0:b+1].focus();break;case"ArrowUp":f.preventDefault(),m[b<=0?m.length-1:b-1].focus();break;case"Home":f.preventDefault(),m[0].focus();break;case"End":f.preventDefault(),m[m.length-1].focus();break;case"Enter":case" ":b>=0&&(f.preventDefault(),m[b].click());break;case"Escape":case"Tab":f.preventDefault(),y();break}}),c.setAttribute("role","menu"),c.setAttribute("aria-label","Test"),n.ownerDocument.body.appendChild(c),E(n,!0)}n.addEventListener("click",x=>{if(x.stopPropagation(),c){h();return}D()});const C=x=>{if(!n.isConnected){n.ownerDocument.removeEventListener("click",C);return}c&&!c.contains(x.target)&&!n.contains(x.target)&&h()};return n.ownerDocument.addEventListener("click",C),Object.defineProperty(n,"value",{get:()=>l!==null?String(l):"",set:x=>{l=x===""?null:Number(x),u()}})}export function makeWaferSelect(e,o,r,t={}){const{maxWidth:i="160px",allLabel:s="All wafers",ownerDocument:l=document}=t,n="\0all",d=l.createElement("select");Object.assign(d.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:i});const p=l.createElement("option");return p.value=n,p.textContent=s,o===null&&(p.selected=!0),d.appendChild(p),e.forEach((u,c)=>{const h=l.createElement("option");h.value=String(c),h.textContent=u.label??`#${c}`,o===c&&(h.selected=!0),d.appendChild(h)}),d.addEventListener("change",()=>r(d.value===n?null:Number(d.value))),d}export function makeToggle(e,o,r,t=document){const i=t.createElement("label");Object.assign(i.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:a.label,cursor:"pointer",userSelect:"none"});const s=t.createElement("input");return s.type="checkbox",s.checked=o,s.style.cssText="margin:0;cursor:pointer;",s.addEventListener("change",()=>r(s.checked)),i.append(s,t.createTextNode(e)),i}export function makeLabeledSelect(e,o,r,t,i={}){const{maxWidth:s="160px",hook:l,ownerDocument:n=document}=i,d=n.createElement("label");d.textContent=e,Object.assign(d.style,{color:a.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const p=n.createElement("select");l&&(p.dataset.wmapSelect=l),Object.assign(p.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:s});for(const u of o){const c=n.createElement("option");c.value=u.value,c.textContent=u.label,u.value===r&&(c.selected=!0),p.appendChild(c)}return p.addEventListener("change",()=>t(p.value)),d.appendChild(p),d}export function makeChartGridWrap(e=document){const o=e.createElement("div");return o.dataset.wmapChartGrid="1",Object.assign(o.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:"10px",flex:"0 0 auto"}),o}export function makeTooltip(e){const o=e.ownerDocument.createElement("div");return Object.assign(o.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",fontFamily:"system-ui, sans-serif",color:a.text,whiteSpace:"nowrap",boxShadow:"0 2px 8px rgba(0,0,0,0.3)"}),e.appendChild(o),o}export function positionChartTooltip(e,o,r,t){const i=o.getBoundingClientRect(),s=14,l=4;e.style.left="0",e.style.top="0";const n=e.offsetWidth,d=e.offsetHeight;let p=r-i.left+s,u=t-i.top+s;p+n+l>i.width&&(p=r-i.left-s-n),u+d+l>i.height&&(u=t-i.top-s-d),p=Math.max(l,p),u=Math.max(l,u),e.style.left=`${p}px`,e.style.top=`${u}px`}
1
+ import{CLR as a,Z_BASE as H,menuLayerFor as N,MENU_SEARCH_THRESHOLD as M,makeMenuSearchBox as I,markMenuTrigger as k,saveImageBlob as z,openReparentedModal as W}from"../toolbar.js";import{ICONS as O}from"../icons.js";import{fmt as F,fmtColorbarAxis as P}from"../../renderer/fmt.js";export function openChartExpandModal(e,r,n){const t=e.getAttribute("style")??"";if(Object.assign(e.style,{flex:"1",minHeight:"0",border:"none",borderRadius:"0"}),!W([e],{title:r,onClosed:()=>{e.setAttribute("style",t),n&&(n.style.display="flex")}})){e.setAttribute("style",t);return}n&&(n.style.display="none")}const _={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040"},j={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border"};export function resolveChartCanvasColors(e){const r=getComputedStyle(e),n={};for(const t of Object.keys(j)){const o=r.getPropertyValue(`--wmap-${j[t]}`).trim();n[t]=o||_[t]}return n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,r){const n=e.ownerDocument.defaultView??window;let t=0;const o=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,r()}))});return o.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,o.disconnect()}}}export function growCardToFitContent(e,r,n){const o=`${e.offsetHeight-r.clientHeight+n}px`;e.style.minHeight!==o&&(e.style.minHeight=o)}export function chartFillHeight(e,r,n,t){let o=0;for(const c of Array.from(r.children))c!==n&&(o+=c.offsetHeight);return growCardToFitContent(e,r,o+t),Math.max(t,r.clientHeight-o)}export function applyCanvasFlow(e,r=0){e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${r}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:F(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,r){if(!r)return{tick:formatValue,unitLabel:""};const{tickFmt:n,axisLabel:t}=P(e,null,r);return{tick:n,unitLabel:t}}export function drawAxisUnit(e,r,n,t,o){const c={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font="10px system-ui, sans-serif",e.textAlign="left",e.textBaseline="middle",e.fillStyle=o,e.fillText(`(${r})`,n,t),Object.assign(e,c)}export function saveCanvasPng(e,r,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const o=t.getContext("2d");o.fillStyle="#ffffff",o.fillRect(0,0,t.width,t.height),o.drawImage(e,0,0),t.toBlob(c=>{c&&z(c,r,n)},"image/png")}export function cardShell(e,r,n=document){const t=n.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"6px",padding:"12px",minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const o=t.ownerDocument.createElement("div");Object.assign(o.style,{display:"flex",alignItems:"center",gap:"6px",marginBottom:"6px"});const c=t.ownerDocument.createElement("div");c.textContent=e,Object.assign(c.style,{color:a.value,fontSize:"13px",fontWeight:"600",flex:"1"}),o.appendChild(c);const p=makeTooltip(t),i=t.ownerDocument.createElement("button");i.type="button",i.setAttribute("aria-label","Save as PNG"),i.innerHTML=O.download,Object.assign(i.style,{border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:"none",color:a.label,cursor:"pointer",width:"22px",height:"22px",lineHeight:"1",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),attachChartTip(i,t,p,"Save as PNG"),i.addEventListener("click",()=>{const l=t.querySelector("canvas");l&&saveCanvasPng(l,e,r)}),o.appendChild(i);const s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Expand"),s.innerHTML=O.expand,Object.assign(s.style,{border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:"none",color:a.label,cursor:"pointer",width:"22px",height:"22px",lineHeight:"1",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),attachChartTip(s,t,p,"Expand"),s.addEventListener("click",()=>openChartExpandModal(t,c.textContent??e,s)),o.appendChild(s),t.appendChild(o);const d=t.ownerDocument.createElement("div");Object.assign(d.style,{display:"flex",gap:"6px",marginBottom:"6px",flexWrap:"wrap",alignItems:"center"}),t.appendChild(d);const u=t.ownerDocument.createElement("div");return Object.assign(u.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(u),{card:t,heading:c,controlsRow:d,body:u}}export function makeSegmented(e,r,n,t=document){const o=t.createElement("div");o.setAttribute("role","radiogroup"),Object.assign(o.style,{display:"inline-flex",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",overflow:"hidden"});const c=`seg-${Math.random().toString(36).slice(2,9)}`,p=[];return e.forEach(([i,s],d)=>{const u=t.createElement("label");Object.assign(u.style,{display:"inline-flex",alignItems:"center",fontSize:"12px",padding:"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:d>0?`1px solid ${a.menuBorder}`:"none"});const l=t.createElement("input");l.type="radio",l.name=c,l.value=i,l.checked=i===r,Object.assign(l.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const h=()=>{u.style.background=l.checked?a.bgActive:a.menuBg,u.style.color=l.checked?a.iconActive:a.text};p.push(h),h(),l.addEventListener("change",()=>{if(l.checked){for(const y of p)y();n(i)}}),u.append(l,t.createTextNode(s)),o.appendChild(u)}),o}export function makeBackButton(e,r=document){const n=r.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{fontSize:"12px",padding:"3px 10px",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:a.menuBg,color:a.label,cursor:"pointer"}),n.addEventListener("click",e),n}export function renderEmptyState(e,r,n){const t=e.ownerDocument.createElement("div");t.textContent=r,Object.assign(t.style,{color:a.label,fontSize:"12px",padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeTestSelect(e,r,n,t={}){const{maxWidth:o="200px",emptyText:c="No parametric tests",ownerDocument:p=document}=t;if(e.length>M)return G(e,r,n,o,p);const i=p.createElement("select");if(Object.assign(i.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:o}),e.length===0){i.disabled=!0;const s=p.createElement("option");s.textContent=c,i.appendChild(s)}else{for(const s of e){const d=p.createElement("option");d.value=String(s.testNumber),d.textContent=s.name||`Test ${s.testNumber}`,s.testNumber===r&&(d.selected=!0),i.appendChild(d)}i.addEventListener("change",()=>n(Number(i.value)))}return i}function G(e,r,n,t,o=document){const c=x=>e.find(v=>v.testNumber===x)?.name||`Test ${x}`;let p=r;const i=o.createElement("button");i.type="button",Object.assign(i.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:t,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:"6px",justifyContent:"space-between"}),k(i,!1);const s=i.ownerDocument.createElement("span");Object.assign(s.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const d=i.ownerDocument.createElement("span");d.textContent="\u25BE",d.style.flex="0 0 auto",i.append(s,d);const u=()=>{s.textContent=p!==null?c(p):"(none)"};u();let l=null;const h=()=>{l&&(l.remove(),l=null,k(i,!1))},y=()=>{h(),i.focus()};function D(){const x=i.getBoundingClientRect(),v=i.ownerDocument.defaultView??window,S=Math.max(x.width,220),R=320,T=Math.min(x.left,Math.max(4,(v.innerWidth??1/0)-S-4));l=i.ownerDocument.createElement("div"),Object.assign(l.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${T}px`,background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",boxShadow:"0 4px 12px rgba(0,0,0,0.15)",zIndex:H,minWidth:`${S}px`,maxHeight:`${R}px`,overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const w=[],L=()=>w.filter(f=>f.row.style.display!=="none").map(f=>f.row),E=I(f=>{for(const m of w)m.row.style.display=m.label.includes(f)?"":"none"},"Filter tests\u2026",i.ownerDocument);Object.assign(E.style,{position:"sticky",top:"0",zIndex:"1",background:a.menuBg}),E.addEventListener("keydown",f=>{if(f.key==="Escape"){y();return}f.key==="ArrowDown"&&(f.preventDefault(),L()[0]?.focus())}),l.appendChild(E);for(const f of e){const m=f.name||`Test ${f.testNumber}`,b=f.testNumber===p,g=i.ownerDocument.createElement("div");g.textContent=m,g.setAttribute("role","menuitemradio"),g.setAttribute("aria-checked",b?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:"6px 14px",fontSize:"12px",cursor:"pointer",color:b?a.iconActive:a.text,fontWeight:b?"700":"400",background:b?a.menuActive:"transparent",whiteSpace:"nowrap",outline:"none"});const A=()=>{b||(g.style.background=a.menuHover)},B=()=>{g.style.background=b?a.menuActive:"transparent"};g.addEventListener("mouseenter",A),g.addEventListener("mouseleave",B),g.addEventListener("focus",A),g.addEventListener("blur",B),g.addEventListener("click",$=>{$.stopPropagation(),p=f.testNumber,u(),y(),n(f.testNumber)}),l.appendChild(g),w.push({row:g,label:m.toLowerCase()})}l.addEventListener("keydown",f=>{const m=L();if(m.length===0)return;const b=m.indexOf(i.ownerDocument.activeElement);switch(f.key){case"ArrowDown":f.preventDefault(),m[b<0||b===m.length-1?0:b+1].focus();break;case"ArrowUp":f.preventDefault(),m[b<=0?m.length-1:b-1].focus();break;case"Home":f.preventDefault(),m[0].focus();break;case"End":f.preventDefault(),m[m.length-1].focus();break;case"Enter":case" ":b>=0&&(f.preventDefault(),m[b].click());break;case"Escape":case"Tab":f.preventDefault(),y();break}}),l.setAttribute("role","menu"),l.setAttribute("aria-label","Test"),N(i).appendChild(l),k(i,!0)}i.addEventListener("click",x=>{if(x.stopPropagation(),l){h();return}D()});const C=x=>{if(!i.isConnected){i.ownerDocument.removeEventListener("click",C);return}l&&!l.contains(x.target)&&!i.contains(x.target)&&h()};return i.ownerDocument.addEventListener("click",C),Object.defineProperty(i,"value",{get:()=>p!==null?String(p):"",set:x=>{p=x===""?null:Number(x),u()}})}export function makeWaferSelect(e,r,n,t={}){const{maxWidth:o="160px",allLabel:c="All wafers",ownerDocument:p=document}=t,i="\0all",s=p.createElement("select");Object.assign(s.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:o});const d=p.createElement("option");return d.value=i,d.textContent=c,r===null&&(d.selected=!0),s.appendChild(d),e.forEach((u,l)=>{const h=p.createElement("option");h.value=String(l),h.textContent=u.label??`#${l}`,r===l&&(h.selected=!0),s.appendChild(h)}),s.addEventListener("change",()=>n(s.value===i?null:Number(s.value))),s}export function makeToggle(e,r,n,t=document){const o=t.createElement("label");Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:a.label,cursor:"pointer",userSelect:"none"});const c=t.createElement("input");return c.type="checkbox",c.checked=r,c.style.cssText="margin:0;cursor:pointer;",c.addEventListener("change",()=>n(c.checked)),o.append(c,t.createTextNode(e)),o}export function makeLabeledSelect(e,r,n,t,o={}){const{maxWidth:c="160px",hook:p,ownerDocument:i=document}=o,s=i.createElement("label");s.textContent=e,Object.assign(s.style,{color:a.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const d=i.createElement("select");p&&(d.dataset.wmapSelect=p),Object.assign(d.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:c});for(const u of r){const l=i.createElement("option");l.value=u.value,l.textContent=u.label,u.value===n&&(l.selected=!0),d.appendChild(l)}return d.addEventListener("change",()=>t(d.value)),s.appendChild(d),s}export function makeChartGridWrap(e=document){const r=e.createElement("div");return r.dataset.wmapChartGrid="1",Object.assign(r.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:"10px",flex:"0 0 auto"}),r}export function makeTooltip(e){const r=e.ownerDocument.createElement("div");return Object.assign(r.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",fontFamily:"system-ui, sans-serif",color:a.text,whiteSpace:"nowrap",boxShadow:"0 2px 8px rgba(0,0,0,0.3)"}),e.appendChild(r),r}export function attachChartTip(e,r,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",o=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,r,o.clientX,o.clientY)}),e.addEventListener("mousemove",o=>{n.style.display==="block"&&positionChartTooltip(n,r,o.clientX,o.clientY)}),e.addEventListener("mouseleave",()=>{n.style.display="none"}),e.addEventListener("focus",()=>{n.textContent=t,n.style.display="block";const o=e.getBoundingClientRect();positionChartTooltip(n,r,o.left+o.width/2,o.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,r,n,t){const o=r.getBoundingClientRect(),c=14,p=4;e.style.left="0",e.style.top="0";const i=e.offsetWidth,s=e.offsetHeight;let d=n-o.left+c,u=t-o.top+c;d+i+p>o.width&&(d=n-o.left-c-i),u+s+p>o.height&&(u=t-o.top-c-s),d=Math.max(p,d),u=Math.max(p,u),e.style.left=`${d}px`,e.style.top=`${u}px`}
@@ -1 +1 @@
1
- import{buildCorrelationMatrix as ut,filterCorrelationMatrix as ft}from"../../stats/correlation.js";import{CORRELATION_POSITIVE as et,CORRELATION_NEGATIVE as nt}from"./palette.js";import{buildFacetTable as mt}from"../../stats/facets.js";import{CLR as g}from"../toolbar.js";import{cardShell as ht,observeResize as xt,makeTooltip as gt,positionChartTooltip as bt,makeLabeledSelect as yt,makeWaferSelect as Mt,renderEmptyState as wt,resolveChartCanvasColors as Lt}from"./chartShell.js";const ot=5,st=100,lt=20;function it(I){const d=I.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(d)return[+d[1],+d[2],+d[3]];const a=I.trim().replace("#","");return a.length===6?[parseInt(a.slice(0,2),16),parseInt(a.slice(2,4),16),parseInt(a.slice(4,6),16)]:null}function vt(I,d,a){const k=it(I);if(!k)return I;const P=Math.round(d[0]+(k[0]-d[0])*a),m=Math.round(d[1]+(k[1]-d[1])*a),D=Math.round(d[2]+(k[2]-d[2])*a);return`rgb(${P},${m},${D})`}export function renderCorrelationPanel(I){const{title:d="Test correlation matrix",items:a,testDefs:k,onSaveImage:P,groups:m,onSelectPair:D}=I,{card:l,body:$,controlsRow:V}=ht(d,P,I.ownerDocument);$.style.overflowX="auto",l.style.alignSelf="start";let F=lt,U=m&&m.length>0?m[0].key:void 0,N=null;const j=l.ownerDocument.createElement("label");j.textContent="Max tests:",j.title="Cap on how many tests the matrix includes (strongest correlations kept first)",Object.assign(j.style,{color:g.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const L=l.ownerDocument.createElement("input");L.type="number",L.min=String(ot),L.max=String(st),L.value=String(F),Object.assign(L.style,{width:"52px",fontSize:"12px",padding:"2px 4px",background:g.menuBg,color:g.value,border:`1px solid ${g.menuBorder}`,borderRadius:"3px"}),L.addEventListener("change",()=>{const e=Math.max(ot,Math.min(st,parseInt(L.value,10)||lt));L.value=String(e),e!==F&&(F=e,G())}),j.appendChild(L),V.appendChild(j),m&&m.length>0?V.appendChild(yt("Group:",m.map(e=>({value:e.key,label:e.key})),U??"",e=>{U=e,G()},{ownerDocument:l.ownerDocument})):a.length>1&&V.appendChild(Mt(a,N,e=>{N=e,G()},{ownerDocument:l.ownerDocument}));function rt(){return m&&m.length>0?m.find(e=>e.key===U)?.items??[]:N!==null?a[N]?[a[N]]:[]:a}const A=l.ownerDocument.createElement("div");Object.assign(A.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),l.insertBefore(A,$);const B=window.devicePixelRatio||1,S=gt(l);let q=()=>{};function at(e,s,c,f,C){if(A.innerHTML="",C.length>0){const y=l.ownerDocument.createElement("div");y.textContent=`\u26A0 Mixed ${C.join(", ")} within this set \u2014 correlations may be misleading (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,Object.assign(y.style,{color:g.warnText,background:g.warnBg,border:`1px solid ${g.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px"}),A.appendChild(y)}const r=l.ownerDocument.createElement("span");Object.assign(r.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:g.label,fontSize:"11px",flexWrap:"wrap"});const E=l.ownerDocument.createElement("span");E.textContent="Pearson r \xB7 click a cell to view that pair in scatter \xB7",r.appendChild(E);const p=l.ownerDocument.createElement("span");Object.assign(p.style,{display:"inline-flex",alignItems:"center",gap:"4px"});const W=l.ownerDocument.createElement("span");W.textContent="\u22121";const b=l.ownerDocument.createElement("span");Object.assign(b.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:"2px",border:`1px solid ${g.menuBorder}`,background:`linear-gradient(to right, ${nt}, ${g.menuBg}, ${et})`});const O=l.ownerDocument.createElement("span");O.textContent="+1",p.append(W,b,O),r.appendChild(p),A.appendChild(r);const T=l.ownerDocument.createElement("span");if(Object.assign(T.style,{color:g.value,fontSize:"12px",fontWeight:"500"}),e===0&&s===0)T.textContent=f?`No significant correlations found \u2014 strongest pair: ${f.xLabel} \u2194 ${f.yLabel} (r = ${f.r.toFixed(2)})`:"No significant correlations found";else{const y=[];e>0&&y.push(`${e} strong (|r| \u2265 0.7)`),s>0&&y.push(`${s} moderate (0.4\u20130.7)`);const H=e+s;let X=y.join(", ")+` pair${H!==1?"s":""} found`;c>0&&(X+=` \xB7 ${c} weak pair${c!==1?"s":""} not shown`),T.textContent=X}A.appendChild(T)}function ct(e){if($.innerHTML="",e.tests.length<2)return wt($,"Need at least two parametric tests for a correlation matrix."),()=>{};const s=l.ownerDocument.createElement("canvas");s.style.display="block",s.style.cursor="default",$.appendChild(s);const c=e.tests.length,f=o=>o.label.split(" (#")[0],C=Math.min(14,Math.max(...e.tests.map(o=>f(o).length))),r=C*6.5+8,E=10,p=Math.round(E*6.5*Math.sin(Math.PI/4))+14,W=14,b=26;let O=-1,T=-1;function y(o){const i=Math.max(0,o-r);return Math.max(W,Math.min(b,Math.floor(i/c)))}const H=new Map;for(const o of e.cells){let i=H.get(o.yIndex);i||(i=[],H.set(o.yIndex,i)),i.push(o)}function X(){const o=Lt(l),i=it(o.bg)??[255,255,255],u=$.clientWidth,n=y(u),_=n*c,v=p+n*c+4,M=r+_;s.width=Math.max(1,Math.floor(M*B)),s.height=Math.max(1,Math.floor(v*B)),s.style.width=`${M}px`,s.style.height=`${v}px`;const t=s.getContext("2d");t.setTransform(B,0,0,B,0,0),t.clearRect(0,0,M,v),t.font="10px system-ui, sans-serif",t.fillStyle=o.text,e.tests.forEach((h,x)=>{const w=f(h),R=w.length>E?`${w.slice(0,E-1)}\u2026`:w,J=r+x*n+n/2;t.save(),t.translate(J,p-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=x===O?o.text:o.textMuted,t.fillText(R,0,0),t.restore()}),e.tests.forEach((h,x)=>{const w=f(h),R=p+x*n,J=R+n/2;t.font="10px system-ui, sans-serif",t.textAlign="right",t.textBaseline="middle",t.fillStyle=x===T?o.text:o.textMuted,t.fillText(w.length>C?`${w.slice(0,C-1)}\u2026`:w,r-4,J),(H.get(x)??[]).forEach(Z=>{const K=Z.xIndex,Y=r+K*n,pt=K===O&&x===T,tt=K===x,z=Z.r;if(z===null){t.fillStyle=o.border,t.globalAlpha=.4,t.fillRect(Y+1,R+1,n-2,n-2),t.globalAlpha=1;return}tt?t.fillStyle=o.bgHover:t.fillStyle=vt(z>=0?et:nt,i,Math.abs(z)),t.fillRect(Y+1,R+1,n-2,n-2),n>=28&&!tt&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Math.abs(z)>.6?o.bg:o.text,t.textAlign="center",t.textBaseline="middle",t.fillText(z.toFixed(2),Y+n/2,R+n/2)),pt&&(t.strokeStyle=o.text,t.lineWidth=2,t.strokeRect(Y+1,R+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=o.bg,t.lineWidth=1;for(let h=0;h<=c;h++){const x=r+h*n;t.beginPath(),t.moveTo(x,p),t.lineTo(x,p+c*n),t.stroke();const w=p+h*n;t.beginPath(),t.moveTo(r,w),t.lineTo(r+c*n,w),t.stroke()}}function Q(o){const i=s.getBoundingClientRect(),u=$.clientWidth,n=y(u),_=(o.clientX-i.left)*(s.width/B/i.width),v=(o.clientY-i.top)*(s.height/B/i.height),M=Math.floor((_-r)/n),t=Math.floor((v-p)/n);return M>=0&&M<c&&t>=0&&t<c?{xi:M,yi:t}:null}return s.addEventListener("mousemove",o=>{const i=Q(o);if(!i){S.style.display="none",s.style.cursor="default";return}const{xi:u,yi:n}=i,_=u===n;s.style.cursor=_||!D?"default":"pointer";const v=e.cells.find(h=>h.xIndex===u&&h.yIndex===n),M=e.tests[u].label,t=e.tests[n].label;_?S.innerHTML=`<strong>${M}</strong>`:v?.r!==null&&v?.r!==void 0?S.innerHTML=`<strong>${f(e.tests[n])}</strong> (#${e.tests[n].testNumber}) vs <strong>${f(e.tests[u])}</strong> (#${e.tests[u].testNumber})<br>r = ${v.r.toFixed(4)}${D?"<br><em>click to view in scatter</em>":""}`:S.innerHTML=`${t} vs ${M}<br><em>insufficient data</em>`,S.style.display="block",bt(S,l,o.clientX,o.clientY)}),s.addEventListener("mouseleave",()=>{S.style.display="none"}),s.addEventListener("click",o=>{const i=Q(o);if(!i||!D)return;const{xi:u,yi:n}=i;u!==n&&(O=u,T=n,X(),D(e.tests[u].testNumber,e.tests[n].testNumber))}),X}function G(){const e=rt(),s=e.flatMap(b=>b.dies??[]),c=ut(s,k),{matrix:f,strongPairs:C,moderatePairs:r,hiddenWeakPairs:E,strongestPair:p}=ft(c,{minTests:6,maxTests:F}),W=mt(e,{facetableOnly:!0}).filter(b=>b.splittable).map(b=>b.label);at(C,r,E,p,W),q=ct(f),q()}const dt=xt(l,()=>q());return G(),{card:l,destroy:()=>dt.disconnect()}}
1
+ import{buildCorrelationMatrix as mt,filterCorrelationMatrix as ht}from"../../stats/correlation.js";import{CORRELATION_POSITIVE as st,CORRELATION_NEGATIVE as lt}from"./palette.js";import{buildFacetTable as xt}from"../../stats/facets.js";import{CLR as b}from"../toolbar.js";import{attachChartTip as gt,cardShell as bt,observeResize as yt,makeTooltip as Mt,positionChartTooltip as wt,makeLabeledSelect as Lt,makeWaferSelect as vt,renderEmptyState as Ct,resolveChartCanvasColors as It}from"./chartShell.js";const it=5,rt=100,at=20;function Z(y){const r=y.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(r)return[+r[1],+r[2],+r[3]];const c=y.trim().replace("#","");return c.length===6?[parseInt(c.slice(0,2),16),parseInt(c.slice(2,4),16),parseInt(c.slice(4,6),16)]:null}function Et(y){const r=Z(y);return r?(r[0]*299+r[1]*587+r[2]*114)/1e3>140?"#111":"#fff":"#000"}function Tt(y,r,c){const k=Z(y);if(!k)return y;const P=Math.round(r[0]+(k[0]-r[0])*c),m=Math.round(r[1]+(k[1]-r[1])*c),D=Math.round(r[2]+(k[2]-r[2])*c);return`rgb(${P},${m},${D})`}export function renderCorrelationPanel(y){const{title:r="Test correlation matrix",items:c,testDefs:k,onSaveImage:P,groups:m,onSelectPair:D}=y,{card:l,body:S,controlsRow:V}=bt(r,P,y.ownerDocument);S.style.overflowX="auto",l.style.alignSelf="start";let j=at,U=m&&m.length>0?m[0].key:void 0,N=null;const H=l.ownerDocument.createElement("label");H.textContent="Max tests:",Object.assign(H.style,{color:b.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const v=l.ownerDocument.createElement("input");v.type="number",v.min=String(it),v.max=String(rt),v.value=String(j),Object.assign(v.style,{width:"52px",fontSize:"12px",padding:"2px 4px",background:b.menuBg,color:b.value,border:`1px solid ${b.menuBorder}`,borderRadius:"3px"}),v.addEventListener("change",()=>{const e=Math.max(it,Math.min(rt,parseInt(v.value,10)||at));v.value=String(e),e!==j&&(j=e,X())}),H.appendChild(v),V.appendChild(H),m&&m.length>0?V.appendChild(Lt("Group:",m.map(e=>({value:e.key,label:e.key})),U??"",e=>{U=e,X()},{ownerDocument:l.ownerDocument})):c.length>1&&V.appendChild(vt(c,N,e=>{N=e,X()},{ownerDocument:l.ownerDocument}));function ct(){return m&&m.length>0?m.find(e=>e.key===U)?.items??[]:N!==null?c[N]?[c[N]]:[]:c}const A=l.ownerDocument.createElement("div");Object.assign(A.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),l.insertBefore(A,S);const B=window.devicePixelRatio||1,I=Mt(l);gt(H,l,I,"Cap on how many tests the matrix includes (strongest correlations kept first)");let q=()=>{};function dt(e,s,d,f,E){if(A.innerHTML="",E.length>0){const h=l.ownerDocument.createElement("div");h.textContent=`\u26A0 Mixed ${E.join(", ")} within this set \u2014 correlations may be misleading (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,Object.assign(h.style,{color:b.warnText,background:b.warnBg,border:`1px solid ${b.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px"}),A.appendChild(h)}const a=l.ownerDocument.createElement("span");Object.assign(a.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:b.label,fontSize:"11px",flexWrap:"wrap"});const T=l.ownerDocument.createElement("span");T.textContent="Pearson r \xB7 click a cell to view that pair in scatter \xB7",a.appendChild(T);const p=l.ownerDocument.createElement("span");Object.assign(p.style,{display:"inline-flex",alignItems:"center",gap:"4px"});const _=l.ownerDocument.createElement("span");_.textContent="\u22121";const M=l.ownerDocument.createElement("span");Object.assign(M.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:"2px",border:`1px solid ${b.menuBorder}`,background:`linear-gradient(to right, ${lt}, ${b.menuBg}, ${st})`});const z=l.ownerDocument.createElement("span");z.textContent="+1",p.append(_,M,z),a.appendChild(p),A.appendChild(a);const $=l.ownerDocument.createElement("span");if(Object.assign($.style,{color:b.value,fontSize:"12px",fontWeight:"500"}),e===0&&s===0)$.textContent=f?`No significant correlations found \u2014 strongest pair: ${f.xLabel} \u2194 ${f.yLabel} (r = ${f.r.toFixed(2)})`:"No significant correlations found";else{const h=[];e>0&&h.push(`${e} strong (|r| \u2265 0.7)`),s>0&&h.push(`${s} moderate (0.4\u20130.7)`);const F=e+s;let W=h.join(", ")+` pair${F!==1?"s":""} found`;d>0&&(W+=` \xB7 ${d} weak pair${d!==1?"s":""} not shown`),$.textContent=W}A.appendChild($)}function pt(e){if(S.innerHTML="",e.tests.length<2)return Ct(S,"Need at least two parametric tests for a correlation matrix."),()=>{};const s=l.ownerDocument.createElement("canvas");s.style.display="block",s.style.cursor="default",S.appendChild(s);const d=e.tests.length,f=o=>o.label.split(" (#")[0],E=Math.min(14,Math.max(...e.tests.map(o=>f(o).length))),a=E*6.5+8,T=10,p=Math.round(T*6.5*Math.sin(Math.PI/4))+14,_=14,M=28,z=44;let $=-1,h=-1;function F(o){const i=Math.max(0,o-a);return Math.max(_,Math.min(z,Math.floor(i/d)))}const W=new Map;for(const o of e.cells){let i=W.get(o.yIndex);i||(i=[],W.set(o.yIndex,i)),i.push(o)}function tt(){const o=It(l),i=Z(o.bg)??[255,255,255],u=S.clientWidth,n=F(u),O=n*d,C=p+n*d+4,w=a+O;s.width=Math.max(1,Math.floor(w*B)),s.height=Math.max(1,Math.floor(C*B)),s.style.width=`${w}px`,s.style.height=`${C}px`;const t=s.getContext("2d");t.setTransform(B,0,0,B,0,0),t.clearRect(0,0,w,C),t.font="10px system-ui, sans-serif",t.fillStyle=o.text,e.tests.forEach((x,g)=>{const L=f(x),R=L.length>T?`${L.slice(0,T-1)}\u2026`:L,J=a+g*n+n/2;t.save(),t.translate(J,p-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=g===$?o.text:o.textMuted,t.fillText(R,0,0),t.restore()}),e.tests.forEach((x,g)=>{const L=f(x),R=p+g*n,J=R+n/2;t.font="10px system-ui, sans-serif",t.textAlign="right",t.textBaseline="middle",t.fillStyle=g===h?o.text:o.textMuted,t.fillText(L.length>E?`${L.slice(0,E-1)}\u2026`:L,a-4,J),(W.get(g)??[]).forEach(nt=>{const K=nt.xIndex,G=a+K*n,ft=K===$&&g===h,ot=K===g,Y=nt.r;if(Y===null){t.fillStyle=o.border,t.globalAlpha=.4,t.fillRect(G+1,R+1,n-2,n-2),t.globalAlpha=1;return}let Q=o.bgHover;ot?t.fillStyle=o.bgHover:(Q=Tt(Y>=0?st:lt,i,Math.abs(Y)),t.fillStyle=Q),t.fillRect(G+1,R+1,n-2,n-2),n>=M&&!ot&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Et(Q),t.textAlign="center",t.textBaseline="middle",t.fillText(Y.toFixed(2),G+n/2,R+n/2)),ft&&(t.strokeStyle=o.text,t.lineWidth=2,t.strokeRect(G+1,R+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=o.bg,t.lineWidth=1;for(let x=0;x<=d;x++){const g=a+x*n;t.beginPath(),t.moveTo(g,p),t.lineTo(g,p+d*n),t.stroke();const L=p+x*n;t.beginPath(),t.moveTo(a,L),t.lineTo(a+d*n,L),t.stroke()}}function et(o){const i=s.getBoundingClientRect(),u=S.clientWidth,n=F(u),O=(o.clientX-i.left)*(s.width/B/i.width),C=(o.clientY-i.top)*(s.height/B/i.height),w=Math.floor((O-a)/n),t=Math.floor((C-p)/n);return w>=0&&w<d&&t>=0&&t<d?{xi:w,yi:t}:null}return s.addEventListener("mousemove",o=>{const i=et(o);if(!i){I.style.display="none",s.style.cursor="default";return}const{xi:u,yi:n}=i,O=u===n;s.style.cursor=O||!D?"default":"pointer";const C=e.cells.find(x=>x.xIndex===u&&x.yIndex===n),w=e.tests[u].label,t=e.tests[n].label;O?I.innerHTML=`<strong>${w}</strong>`:C?.r!==null&&C?.r!==void 0?I.innerHTML=`<strong>${f(e.tests[n])}</strong> (#${e.tests[n].testNumber}) vs <strong>${f(e.tests[u])}</strong> (#${e.tests[u].testNumber})<br>r = ${C.r.toFixed(4)}${D?"<br><em>click to view in scatter</em>":""}`:I.innerHTML=`${t} vs ${w}<br><em>insufficient data</em>`,I.style.display="block",wt(I,l,o.clientX,o.clientY)}),s.addEventListener("mouseleave",()=>{I.style.display="none"}),s.addEventListener("click",o=>{const i=et(o);if(!i||!D)return;const{xi:u,yi:n}=i;u!==n&&($=u,h=n,tt(),D(e.tests[u].testNumber,e.tests[n].testNumber))}),tt}function X(){const e=ct(),s=e.flatMap(M=>M.dies??[]),d=mt(s,k),{matrix:f,strongPairs:E,moderatePairs:a,hiddenWeakPairs:T,strongestPair:p}=ht(d,{minTests:6,maxTests:j}),_=xt(e,{facetableOnly:!0}).filter(M=>M.splittable).map(M=>M.label);dt(E,a,T,p,_),q=pt(f),q()}const ut=yt(l,()=>q());return X(),{card:l,destroy:()=>ut.disconnect()}}
@@ -1 +1 @@
1
- import{buildTestHistogramData as Ht,buildTestHistogramSeries as At}from"../../stats/histogram.js";import{CLR as lt}from"../toolbar.js";import{fmt as st}from"../../renderer/fmt.js";import{QUANTITY as Wt,categorical as Ct}from"./palette.js";import{cardShell as Dt,observeResize as xt,makeTooltip as Et,positionChartTooltip as bt,makeTestSelect as Pt,makeWaferSelect as $t,makeToggle as Bt,renderEmptyState as pt,chartFillHeight as yt,applyCanvasFlow as Mt,resolveChartCanvasColors as Tt,makeAxisFormat as wt,PADDING as tt}from"./chartShell.js";const mt=230,vt=36,at=18;function St(l,B,N,z,rt,R,i,f=4){const j=Math.max(1,R/f),F=Math.pow(10,Math.floor(Math.log10(j))),x=j/F,G=(x<=1?1:x<=2?2:x<=5?5:10)*F,P=z-N;l.save(),l.font="10px system-ui, sans-serif",l.textAlign="right",l.textBaseline="middle";for(let I=0;I<=R+1e-9;I+=G){const _=z-I/(R||1)*P;l.strokeStyle=i.grid,l.lineWidth=1,l.beginPath(),l.moveTo(B,_),l.lineTo(B+rt,_),l.stroke(),l.fillStyle=i.text,l.fillText(String(Math.round(I)),B-4,_)}l.strokeStyle=i.axis,l.lineWidth=1,l.beginPath(),l.moveTo(B,N),l.lineTo(B,z),l.stroke(),l.restore()}export function renderHistogramPanel(l){const{title:B="Test value distribution",items:N,testDefs:z,onSaveImage:rt,groups:R}=l,{card:i,body:f,controlsRow:j}=Dt(B,rt,l.ownerDocument),F=z.filter(r=>r.testNumber!==void 0);let x=l.selectedTestNumber??F[0]?.testNumber??null,G=null,P=!1;const I=Pt(F,x,r=>{x=r,U()},{maxWidth:"200px",ownerDocument:i.ownerDocument});j.appendChild(I);const _=$t(N,G,r=>{G=r,U()},{ownerDocument:i.ownerDocument});j.appendChild(_),j.appendChild(Bt("Axis includes limits",P,r=>{P=r,U()},i.ownerDocument));const A=Et(i);let J=null,y=null;function et(r){const L=z.find(K=>K.testNumber===r);return{unit:L?.unit,limitLow:L?.limitLow,limitHigh:L?.limitHigh}}function U(){if(J?.disconnect(),J=null,f.innerHTML="",x===null){pt(f,"No parametric test data available for a histogram.");return}const r=R&&R.length>0?At(R,x,16,P?et(x).limitLow:void 0,P?et(x).limitHigh:void 0):null;if(_.style.display=r?"none":"",r){if(r.series.length===0){pt(f,"No parametric test data available for a histogram.");return}kt(r);return}const L=G!==null?[N[G]]:N,{unit:K,limitLow:nt,limitHigh:b}=et(x),s=Ht(L,x,16,P?nt:void 0,P?b:void 0);if(s.length===0){pt(f,"No parametric test data available for a histogram.");return}const O=Math.max(...s.map(m=>m.count),1),E=i.ownerDocument.createElement("div");Object.assign(E.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),E.textContent=`max ${O} dies/bucket`,f.appendChild(E);const g=i.ownerDocument.createElement("canvas");g.style.display="block",g.style.cursor="default",f.appendChild(g);let w=-1;const p=window.devicePixelRatio||1,$=s[0].rangeLow,X=s[s.length-1].rangeHigh,ot=X-$||1,Z=wt(Math.max(Math.abs($),Math.abs(X)),K);function Y(m){const u=tt+36,e=g.clientWidth-u-tt,n=m-vt-at;return{plotX:u,plotMaxWidth:Math.max(10,e),plotMaxHeight:n,plotTop:at}}function ct(m){const{plotX:u,plotMaxWidth:e}=Y(mt);if(m<u||m>u+e)return-1;const n=Math.floor((m-u)/e*s.length);return n>=0&&n<s.length?n:-1}function H(){Mt(g,E.offsetHeight);const m=f.clientWidth,u=yt(i,f,g,mt);g.width=Math.max(1,Math.floor(m*p)),g.height=Math.max(1,Math.floor(u*p)),g.style.width=`${m}px`,g.style.height=`${u}px`;const e=g.getContext("2d");e.setTransform(p,0,0,p,0,0),e.clearRect(0,0,m,u),e.font="11px system-ui, sans-serif";const n=Tt(i),{plotX:d,plotMaxWidth:t,plotMaxHeight:h,plotTop:o}=Y(u),a=o+h,v=t/s.length;St(e,d,o,a,t,O,{text:n.textMuted,axis:n.border,grid:n.border}),s.forEach((M,C)=>{const S=M.count/O*h,c=d+C*v,k=a-S;e.fillStyle=C===w?n.text:Wt,e.globalAlpha=C===w?1:.7,e.fillRect(c+1,k,Math.max(1,v-2),S),e.globalAlpha=1,C===w&&(e.fillStyle=n.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${M.count}`,c+v/2,k-2))});const V=M=>d+(M-$)/ot*t;e.font="10px system-ui, sans-serif";for(const[M,C]of[[nt,"LSL"],[b,"USL"]]){if(M===void 0)continue;const S=V(M);e.strokeStyle=n.warnBorder,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(S,o),e.lineTo(S,a),e.stroke(),e.setLineDash([]),e.fillStyle=n.warnBorder,e.textAlign="center",e.textBaseline="bottom",e.fillText(C,S,o-2)}e.font="11px system-ui, sans-serif",e.strokeStyle=n.border,e.lineWidth=1,e.beginPath(),e.moveTo(d,a),e.lineTo(d+t,a),e.stroke(),e.fillStyle=n.textMuted,e.textAlign="center",e.textBaseline="top";const W=Math.max(2,Math.floor(t/44)),dt=Math.ceil(s.length/W),ht=Math.max(1,dt);for(let M=0;M<=s.length;M+=ht){const C=M<s.length?s[M].rangeLow:s[s.length-1].rangeHigh,S=d+M*v;e.beginPath(),e.moveTo(S,a),e.lineTo(S,a+4),e.stroke(),e.fillText(Z.tick(C),S,a+6)}Z.unitLabel&&(e.save(),e.font="10px system-ui, sans-serif",e.fillStyle=n.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${Z.unitLabel})`,d+t,a+20),e.restore())}g.addEventListener("mousemove",m=>{const u=g.getBoundingClientRect(),e=ct(m.clientX-u.left);if(e!==w&&(w=e,H()),e>=0){const n=s[e];A.innerHTML=`<strong>${st(n.rangeLow,K,"engineering")} \u2013 ${st(n.rangeHigh,K,"engineering")}</strong><br>${n.count} dies`,A.style.display="block",bt(A,i,m.clientX,m.clientY)}else A.style.display="none"}),g.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,H()),A.style.display="none"}),J=xt(i,()=>H()),H()}function kt(r){const{unit:L,limitLow:K,limitHigh:nt}=et(x),b=r.ranges,s=r.series,O=Ct;y&&!s.some(n=>n.groupKey===y)&&(y=null);const E=Math.max(1,...s.flatMap(n=>n.counts)),g=i.ownerDocument.createElement("div");Object.assign(g.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),g.textContent=`${s.length} groups \xB7 max ${E} dies/bucket`,f.appendChild(g);const w=i.ownerDocument.createElement("div");Object.assign(w.style,{display:"flex",flexWrap:"wrap",gap:"4px 12px",marginBottom:"4px"}),s.forEach((n,d)=>{const t=i.ownerDocument.createElement("button");t.type="button",t.title=`${n.groupKey} \u2014 click to emphasize (dim the rest)`,Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:"5px",fontSize:"11px",padding:"1px 4px",border:"none",background:"none",cursor:"pointer",color:lt.text,borderRadius:"3px"});const h=i.ownerDocument.createElement("span");Object.assign(h.style,{width:"10px",height:"10px",borderRadius:"2px",background:O(d),flex:"0 0 auto"});const o=i.ownerDocument.createElement("span");o.textContent=n.groupKey,t.append(h,o);const a=y!==null&&y!==n.groupKey;t.style.opacity=a?"0.45":"1",y===n.groupKey&&(t.style.background=lt.bgHover),t.setAttribute("aria-pressed",y===n.groupKey?"true":"false"),t.addEventListener("click",()=>{y=y===n.groupKey?null:n.groupKey,U()}),w.appendChild(t)}),f.appendChild(w);const p=i.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",f.appendChild(p);const $=window.devicePixelRatio||1,X=b[0].rangeLow,ot=b[b.length-1].rangeHigh,Z=ot-X||1,Y=wt(Math.max(Math.abs(X),Math.abs(ot)),L),ct=()=>g.offsetHeight+w.offsetHeight;let H=-1,m={plotX:tt+36,plotMaxWidth:10,barWidth:10};function u(){Mt(p,ct());const n=f.clientWidth,d=yt(i,f,p,mt);p.width=Math.max(1,Math.floor(n*$)),p.height=Math.max(1,Math.floor(d*$)),p.style.width=`${n}px`,p.style.height=`${d}px`;const t=p.getContext("2d");t.setTransform($,0,0,$,0,0),t.clearRect(0,0,n,d),t.font="11px system-ui, sans-serif";const h=Tt(i),o=tt+36,a=Math.max(10,n-o-tt),v=at,V=d-vt-at,T=v+V,W=a/b.length;m={plotX:o,plotMaxWidth:a,barWidth:W},St(t,o,v,T,a,E,{text:h.textMuted,axis:h.border,grid:h.border}),H>=0&&(t.fillStyle=h.bgHover,t.globalAlpha=.5,t.fillRect(o+H*W,v,W,V),t.globalAlpha=1);const dt=s.map((c,k)=>({s:c,i:k})).sort((c,k)=>(c.s.groupKey===y?1:0)-(k.s.groupKey===y?1:0));for(const{s:c,i:k}of dt){const D=y===c.groupKey,ut=y!==null&&!D,ft=O(k);t.strokeStyle=ft,t.fillStyle=ft,t.lineWidth=D?2:1,t.globalAlpha=ut?.12:y?.32:.22,t.beginPath(),t.moveTo(o,T),c.counts.forEach((gt,it)=>{const Q=o+it*W,q=T-gt/E*V;t.lineTo(Q,q),t.lineTo(Q+W,q)}),t.lineTo(o+a,T),t.closePath(),t.fill(),t.globalAlpha=ut?.3:1,t.beginPath(),c.counts.forEach((gt,it)=>{const Q=o+it*W,q=T-gt/E*V;it===0?t.moveTo(Q,q):t.lineTo(Q,q),t.lineTo(Q+W,q)}),t.stroke()}t.globalAlpha=1;const ht=c=>o+(c-X)/Z*a;t.font="10px system-ui, sans-serif";for(const[c,k]of[[K,"LSL"],[nt,"USL"]]){if(c===void 0)continue;const D=ht(c);t.strokeStyle=h.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(D,v),t.lineTo(D,T),t.stroke(),t.setLineDash([]),t.fillStyle=h.warnBorder,t.textAlign="center",t.textBaseline="bottom",t.fillText(k,D,v-2)}t.font="11px system-ui, sans-serif",t.strokeStyle=h.border,t.lineWidth=1,t.beginPath(),t.moveTo(o,T),t.lineTo(o+a,T),t.stroke(),t.fillStyle=h.textMuted,t.textAlign="center",t.textBaseline="top";const C=Math.max(2,Math.floor(a/44)),S=Math.max(1,Math.ceil(b.length/C));for(let c=0;c<=b.length;c+=S){const k=c<b.length?b[c].rangeLow:b[b.length-1].rangeHigh,D=o+c*W;t.beginPath(),t.moveTo(D,T),t.lineTo(D,T+4),t.stroke(),t.fillText(Y.tick(k),D,T+6)}Y.unitLabel&&(t.save(),t.font="10px system-ui, sans-serif",t.fillStyle=h.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${Y.unitLabel})`,o+a,T+20),t.restore())}function e(n){const{plotX:d,plotMaxWidth:t,barWidth:h}=m;if(n<d||n>d+t)return-1;const o=Math.floor((n-d)/h);return o>=0&&o<b.length?o:-1}p.addEventListener("mousemove",n=>{const d=p.getBoundingClientRect(),t=e(n.clientX-d.left);if(t!==H&&(H=t,u()),t>=0){const h=b[t],o=s.map((a,v)=>`<span style="display:inline-block;width:9px;height:9px;border-radius:2px;background:${O(v)};margin-right:5px;"></span>${a.groupKey}: ${a.counts[t]}`).join("<br>");A.innerHTML=`<strong>${st(h.rangeLow,L,"engineering")} \u2013 ${st(h.rangeHigh,L,"engineering")}</strong><br>${o}`,A.style.display="block",bt(A,i,n.clientX,n.clientY)}else A.style.display="none"}),p.addEventListener("mouseleave",()=>{H!==-1&&(H=-1,u()),A.style.display="none"}),J=xt(i,()=>u()),u()}U();function Lt(r){!F.some(L=>L.testNumber===r)||r===x||(x=r,I.value=String(r),U())}return{card:i,setTest:Lt,destroy:()=>J?.disconnect()}}
1
+ import{buildTestHistogramData as Ht,buildTestHistogramSeries as At}from"../../stats/histogram.js";import{CLR as lt}from"../toolbar.js";import{fmt as st}from"../../renderer/fmt.js";import{QUANTITY as Ct,categorical as Wt}from"./palette.js";import{cardShell as Dt,observeResize as xt,makeTooltip as Et,attachChartTip as Pt,positionChartTooltip as bt,makeTestSelect as $t,makeWaferSelect as Bt,makeToggle as Rt,renderEmptyState as pt,chartFillHeight as yt,applyCanvasFlow as Mt,resolveChartCanvasColors as Tt,makeAxisFormat as wt,PADDING as tt}from"./chartShell.js";const mt=230,vt=36,at=18;function St(l,B,N,z,rt,R,i,f=4){const j=Math.max(1,R/f),F=Math.pow(10,Math.floor(Math.log10(j))),x=j/F,G=(x<=1?1:x<=2?2:x<=5?5:10)*F,P=z-N;l.save(),l.font="10px system-ui, sans-serif",l.textAlign="right",l.textBaseline="middle";for(let I=0;I<=R+1e-9;I+=G){const _=z-I/(R||1)*P;l.strokeStyle=i.grid,l.lineWidth=1,l.beginPath(),l.moveTo(B,_),l.lineTo(B+rt,_),l.stroke(),l.fillStyle=i.text,l.fillText(String(Math.round(I)),B-4,_)}l.strokeStyle=i.axis,l.lineWidth=1,l.beginPath(),l.moveTo(B,N),l.lineTo(B,z),l.stroke(),l.restore()}export function renderHistogramPanel(l){const{title:B="Test value distribution",items:N,testDefs:z,onSaveImage:rt,groups:R}=l,{card:i,body:f,controlsRow:j}=Dt(B,rt,l.ownerDocument),F=z.filter(r=>r.testNumber!==void 0);let x=l.selectedTestNumber??F[0]?.testNumber??null,G=null,P=!1;const I=$t(F,x,r=>{x=r,U()},{maxWidth:"200px",ownerDocument:i.ownerDocument});j.appendChild(I);const _=Bt(N,G,r=>{G=r,U()},{ownerDocument:i.ownerDocument});j.appendChild(_),j.appendChild(Rt("Axis includes limits",P,r=>{P=r,U()},i.ownerDocument));const L=Et(i);let J=null,y=null;function et(r){const H=z.find(K=>K.testNumber===r);return{unit:H?.unit,limitLow:H?.limitLow,limitHigh:H?.limitHigh}}function U(){if(J?.disconnect(),J=null,f.innerHTML="",x===null){pt(f,"No parametric test data available for a histogram.");return}const r=R&&R.length>0?At(R,x,16,P?et(x).limitLow:void 0,P?et(x).limitHigh:void 0):null;if(_.style.display=r?"none":"",r){if(r.series.length===0){pt(f,"No parametric test data available for a histogram.");return}kt(r);return}const H=G!==null?[N[G]]:N,{unit:K,limitLow:nt,limitHigh:b}=et(x),s=Ht(H,x,16,P?nt:void 0,P?b:void 0);if(s.length===0){pt(f,"No parametric test data available for a histogram.");return}const O=Math.max(...s.map(m=>m.count),1),E=i.ownerDocument.createElement("div");Object.assign(E.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),E.textContent=`max ${O} dies/bucket`,f.appendChild(E);const g=i.ownerDocument.createElement("canvas");g.style.display="block",g.style.cursor="default",f.appendChild(g);let w=-1;const p=window.devicePixelRatio||1,$=s[0].rangeLow,X=s[s.length-1].rangeHigh,ot=X-$||1,Z=wt(Math.max(Math.abs($),Math.abs(X)),K);function Y(m){const u=tt+36,e=g.clientWidth-u-tt,n=m-vt-at;return{plotX:u,plotMaxWidth:Math.max(10,e),plotMaxHeight:n,plotTop:at}}function ct(m){const{plotX:u,plotMaxWidth:e}=Y(mt);if(m<u||m>u+e)return-1;const n=Math.floor((m-u)/e*s.length);return n>=0&&n<s.length?n:-1}function A(){Mt(g,E.offsetHeight);const m=f.clientWidth,u=yt(i,f,g,mt);g.width=Math.max(1,Math.floor(m*p)),g.height=Math.max(1,Math.floor(u*p)),g.style.width=`${m}px`,g.style.height=`${u}px`;const e=g.getContext("2d");e.setTransform(p,0,0,p,0,0),e.clearRect(0,0,m,u),e.font="11px system-ui, sans-serif";const n=Tt(i),{plotX:d,plotMaxWidth:t,plotMaxHeight:h,plotTop:o}=Y(u),a=o+h,v=t/s.length;St(e,d,o,a,t,O,{text:n.textMuted,axis:n.border,grid:n.border}),s.forEach((M,W)=>{const S=M.count/O*h,c=d+W*v,k=a-S;e.fillStyle=W===w?n.text:Ct,e.globalAlpha=W===w?1:.7,e.fillRect(c+1,k,Math.max(1,v-2),S),e.globalAlpha=1,W===w&&(e.fillStyle=n.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${M.count}`,c+v/2,k-2))});const V=M=>d+(M-$)/ot*t;e.font="10px system-ui, sans-serif";for(const[M,W]of[[nt,"LSL"],[b,"USL"]]){if(M===void 0)continue;const S=V(M);e.strokeStyle=n.warnBorder,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(S,o),e.lineTo(S,a),e.stroke(),e.setLineDash([]),e.fillStyle=n.warnBorder,e.textAlign="center",e.textBaseline="bottom",e.fillText(W,S,o-2)}e.font="11px system-ui, sans-serif",e.strokeStyle=n.border,e.lineWidth=1,e.beginPath(),e.moveTo(d,a),e.lineTo(d+t,a),e.stroke(),e.fillStyle=n.textMuted,e.textAlign="center",e.textBaseline="top";const C=Math.max(2,Math.floor(t/44)),dt=Math.ceil(s.length/C),ht=Math.max(1,dt);for(let M=0;M<=s.length;M+=ht){const W=M<s.length?s[M].rangeLow:s[s.length-1].rangeHigh,S=d+M*v;e.beginPath(),e.moveTo(S,a),e.lineTo(S,a+4),e.stroke(),e.fillText(Z.tick(W),S,a+6)}Z.unitLabel&&(e.save(),e.font="10px system-ui, sans-serif",e.fillStyle=n.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${Z.unitLabel})`,d+t,a+20),e.restore())}g.addEventListener("mousemove",m=>{const u=g.getBoundingClientRect(),e=ct(m.clientX-u.left);if(e!==w&&(w=e,A()),e>=0){const n=s[e];L.innerHTML=`<strong>${st(n.rangeLow,K,"engineering")} \u2013 ${st(n.rangeHigh,K,"engineering")}</strong><br>${n.count} dies`,L.style.display="block",bt(L,i,m.clientX,m.clientY)}else L.style.display="none"}),g.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,A()),L.style.display="none"}),J=xt(i,()=>A()),A()}function kt(r){const{unit:H,limitLow:K,limitHigh:nt}=et(x),b=r.ranges,s=r.series,O=Wt;y&&!s.some(n=>n.groupKey===y)&&(y=null);const E=Math.max(1,...s.flatMap(n=>n.counts)),g=i.ownerDocument.createElement("div");Object.assign(g.style,{fontSize:"12px",color:lt.label,marginBottom:"2px"}),g.textContent=`${s.length} groups \xB7 max ${E} dies/bucket`,f.appendChild(g);const w=i.ownerDocument.createElement("div");Object.assign(w.style,{display:"flex",flexWrap:"wrap",gap:"4px 12px",marginBottom:"4px"}),s.forEach((n,d)=>{const t=i.ownerDocument.createElement("button");t.type="button",Pt(t,i,L,`${n.groupKey} \u2014 click to emphasize (dim the rest)`),Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:"5px",fontSize:"11px",padding:"1px 4px",border:"none",background:"none",cursor:"pointer",color:lt.text,borderRadius:"3px"});const h=i.ownerDocument.createElement("span");Object.assign(h.style,{width:"10px",height:"10px",borderRadius:"2px",background:O(d),flex:"0 0 auto"});const o=i.ownerDocument.createElement("span");o.textContent=n.groupKey,t.append(h,o);const a=y!==null&&y!==n.groupKey;t.style.opacity=a?"0.45":"1",y===n.groupKey&&(t.style.background=lt.bgHover),t.setAttribute("aria-pressed",y===n.groupKey?"true":"false"),t.addEventListener("click",()=>{y=y===n.groupKey?null:n.groupKey,U()}),w.appendChild(t)}),f.appendChild(w);const p=i.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",f.appendChild(p);const $=window.devicePixelRatio||1,X=b[0].rangeLow,ot=b[b.length-1].rangeHigh,Z=ot-X||1,Y=wt(Math.max(Math.abs(X),Math.abs(ot)),H),ct=()=>g.offsetHeight+w.offsetHeight;let A=-1,m={plotX:tt+36,plotMaxWidth:10,barWidth:10};function u(){Mt(p,ct());const n=f.clientWidth,d=yt(i,f,p,mt);p.width=Math.max(1,Math.floor(n*$)),p.height=Math.max(1,Math.floor(d*$)),p.style.width=`${n}px`,p.style.height=`${d}px`;const t=p.getContext("2d");t.setTransform($,0,0,$,0,0),t.clearRect(0,0,n,d),t.font="11px system-ui, sans-serif";const h=Tt(i),o=tt+36,a=Math.max(10,n-o-tt),v=at,V=d-vt-at,T=v+V,C=a/b.length;m={plotX:o,plotMaxWidth:a,barWidth:C},St(t,o,v,T,a,E,{text:h.textMuted,axis:h.border,grid:h.border}),A>=0&&(t.fillStyle=h.bgHover,t.globalAlpha=.5,t.fillRect(o+A*C,v,C,V),t.globalAlpha=1);const dt=s.map((c,k)=>({s:c,i:k})).sort((c,k)=>(c.s.groupKey===y?1:0)-(k.s.groupKey===y?1:0));for(const{s:c,i:k}of dt){const D=y===c.groupKey,ut=y!==null&&!D,ft=O(k);t.strokeStyle=ft,t.fillStyle=ft,t.lineWidth=D?2:1,t.globalAlpha=ut?.12:y?.32:.22,t.beginPath(),t.moveTo(o,T),c.counts.forEach((gt,it)=>{const Q=o+it*C,q=T-gt/E*V;t.lineTo(Q,q),t.lineTo(Q+C,q)}),t.lineTo(o+a,T),t.closePath(),t.fill(),t.globalAlpha=ut?.3:1,t.beginPath(),c.counts.forEach((gt,it)=>{const Q=o+it*C,q=T-gt/E*V;it===0?t.moveTo(Q,q):t.lineTo(Q,q),t.lineTo(Q+C,q)}),t.stroke()}t.globalAlpha=1;const ht=c=>o+(c-X)/Z*a;t.font="10px system-ui, sans-serif";for(const[c,k]of[[K,"LSL"],[nt,"USL"]]){if(c===void 0)continue;const D=ht(c);t.strokeStyle=h.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(D,v),t.lineTo(D,T),t.stroke(),t.setLineDash([]),t.fillStyle=h.warnBorder,t.textAlign="center",t.textBaseline="bottom",t.fillText(k,D,v-2)}t.font="11px system-ui, sans-serif",t.strokeStyle=h.border,t.lineWidth=1,t.beginPath(),t.moveTo(o,T),t.lineTo(o+a,T),t.stroke(),t.fillStyle=h.textMuted,t.textAlign="center",t.textBaseline="top";const W=Math.max(2,Math.floor(a/44)),S=Math.max(1,Math.ceil(b.length/W));for(let c=0;c<=b.length;c+=S){const k=c<b.length?b[c].rangeLow:b[b.length-1].rangeHigh,D=o+c*C;t.beginPath(),t.moveTo(D,T),t.lineTo(D,T+4),t.stroke(),t.fillText(Y.tick(k),D,T+6)}Y.unitLabel&&(t.save(),t.font="10px system-ui, sans-serif",t.fillStyle=h.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${Y.unitLabel})`,o+a,T+20),t.restore())}function e(n){const{plotX:d,plotMaxWidth:t,barWidth:h}=m;if(n<d||n>d+t)return-1;const o=Math.floor((n-d)/h);return o>=0&&o<b.length?o:-1}p.addEventListener("mousemove",n=>{const d=p.getBoundingClientRect(),t=e(n.clientX-d.left);if(t!==A&&(A=t,u()),t>=0){const h=b[t],o=s.map((a,v)=>`<span style="display:inline-block;width:9px;height:9px;border-radius:2px;background:${O(v)};margin-right:5px;"></span>${a.groupKey}: ${a.counts[t]}`).join("<br>");L.innerHTML=`<strong>${st(h.rangeLow,H,"engineering")} \u2013 ${st(h.rangeHigh,H,"engineering")}</strong><br>${o}`,L.style.display="block",bt(L,i,n.clientX,n.clientY)}else L.style.display="none"}),p.addEventListener("mouseleave",()=>{A!==-1&&(A=-1,u()),L.style.display="none"}),J=xt(i,()=>u()),u()}U();function Lt(r){!F.some(H=>H.testNumber===r)||r===x||(x=r,I.value=String(r),U())}return{card:i,setTest:Lt,destroy:()=>J?.disconnect()}}
@@ -1 +1 @@
1
- import{getColorScheme as he}from"../../renderer/colorSchemes.js";import{categorical as ge}from"./palette.js";import{buildScatterData as be,buildScatterDataGrouped as ye}from"../../stats/scatter.js";import{buildFacetTable as we}from"../../stats/facets.js";import{CLR as M}from"../toolbar.js";import{cardShell as Se,observeResize as Me,makeTooltip as Te,makeTestSelect as ke,makeWaferSelect as ve,chartFillHeight as Le,applyCanvasFlow as Ce,drawAxisUnit as Ae,resolveChartCanvasColors as De,makeAxisFormat as ne}from"./chartShell.js";const c=52,Be=16,a=16,Oe=44;export function renderScatterPanel(W){const{title:oe="Test scatter",items:D,testDefs:_,colorScheme:ie="default",onSaveImage:le,groups:P}=W,{card:i,body:C,controlsRow:X}=Se(oe,le,W.ownerDocument),R=_.filter(t=>t.testNumber!==void 0),y=!!(P&&P.length>0),Y=(P??[]).map(t=>t.key),ae=new Map(Y.map((t,n)=>[t,n]));let x=W.xTestNumber??R[0]?.testNumber??null,f=W.yTestNumber??R[1]?.testNumber??x,B=null;const q=()=>B!==null?D[B]?[D[B]]:[]:D;function K(t,n,o){const l=i.ownerDocument.createElement("label");Object.assign(l.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:M.label});const p=i.ownerDocument.createElement("span");p.textContent=t;const m=ke(R,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:i.ownerDocument});return l.append(p,m),{wrap:l,select:m}}const{wrap:se,select:re}=K("X:",x,t=>{x=t,E()}),{wrap:ce,select:de}=K("Y:",f,t=>{f=t,E()});X.append(se,ce),!y&&D.length>1&&X.appendChild(ve(D,B,t=>{B=t,E()},{ownerDocument:i.ownerDocument}));const O=i.ownerDocument.createElement("div");Object.assign(O.style,{color:M.warnText,background:M.warnBg,border:`1px solid ${M.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",marginBottom:"4px",display:"none"}),i.insertBefore(O,C);function J(){const t=y?[]:we(q(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);t.length>0?(O.textContent=`\u26A0 Mixed ${t.join(", ")} across these points \u2014 trends here may be confounded (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,O.style.display=""):O.style.display="none"}J();const j=i.ownerDocument.createElement("div");j.textContent=y?"One point per die \xB7 coloured by group \xB7 click legend to filter":"One point per die across all wafers \xB7 coloured by hard bin \xB7 click legend to filter",Object.assign(j.style,{color:M.label,fontSize:"11px",marginBottom:"4px"}),i.insertBefore(j,C);const pe=he(ie),{forBin:me}=pe,G=t=>y?t.group??"\u2014":String(t.hbin??0),Q=t=>y?ge(ae.get(t)??0):me(Number(t)),V=t=>y?t:t==="0"?"No bin data":`Bin ${t}`,b=new Set,A=i.ownerDocument.createElement("div");Object.assign(A.style,{display:"flex",flexWrap:"wrap",gap:"4px",marginBottom:"6px"}),C.appendChild(A);const h=i.ownerDocument.createElement("canvas");h.style.display="block",h.style.cursor="crosshair",C.appendChild(h);const F=window.devicePixelRatio||1,Ee=Te(i);let g=[],T=0,I=1,k=0,N=1;function $(t){const n=_.find(o=>o.testNumber===t);return{unit:n?.unit,limitLow:n?.limitLow,limitHigh:n?.limitHigh}}function xe(){const t=C.clientWidth,n=Math.max(200,Math.min(400,t*.65)),o=Le(i,C,h,n);return{w:t,h:o,plotW:Math.max(10,t-c-Be),plotH:Math.max(10,o-a-Oe)}}function Z(){for(const t of A.querySelectorAll("[data-cat]")){const n=t.dataset.cat,o=b.size===0||b.has(n);t.style.opacity=o?"1":"0.35";const l=b.has(n);t.style.boxShadow=l?`0 0 0 2px ${M.text}`:"none",t.setAttribute("aria-pressed",l?"true":"false")}}function ee(t){A.innerHTML="",b.clear();for(const n of t){const o=i.ownerDocument.createElement("button");o.type="button",o.dataset.cat=n,o.title=`${V(n)} \u2014 click to filter`;const l=Q(n);Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:"4px",padding:"2px 7px",borderRadius:"10px",border:`1px solid ${M.menuBorder}`,background:"none",cursor:"pointer",fontSize:"11px",color:M.text,whiteSpace:"nowrap"});const p=i.ownerDocument.createElement("span");Object.assign(p.style,{display:"inline-block",width:"9px",height:"9px",borderRadius:"50%",background:l,flexShrink:"0"}),o.append(p,i.ownerDocument.createTextNode(V(n))),o.addEventListener("click",()=>{b.has(n)?b.delete(n):b.add(n),Z(),z()}),A.appendChild(o)}Z()}function z(){Ce(h,A.offsetHeight);const t=De(i),n=I-T,o=N-k,{w:l,h:p,plotW:m,plotH:d}=xe();h.width=Math.max(1,Math.floor(l*F)),h.height=Math.max(1,Math.floor(p*F)),h.style.width=`${l}px`,h.style.height=`${p}px`;const e=h.getContext("2d");if(e.setTransform(F,0,0,F,0,0),e.clearRect(0,0,l,p),g.length===0){e.font="12px system-ui, sans-serif",e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.fillText("No data \u2014 select two parametric tests with values.",l/2,p/2);return}const w=4,v=ne(Math.max(Math.abs(T),Math.abs(I)),x!==null?$(x).unit:void 0),U=ne(Math.max(Math.abs(k),Math.abs(N)),f!==null?$(f).unit:void 0);e.font="10px system-ui, sans-serif",e.strokeStyle=t.border,e.lineWidth=.5,e.fillStyle=t.textMuted;for(let s=0;s<=w;s++){const S=T+n*s/w,r=c+s/w*m;e.beginPath(),e.moveTo(r,a),e.lineTo(r,a+d),e.stroke(),e.textAlign="center",e.textBaseline="top",e.fillText(v.tick(S),r,a+d+4);const L=k+o*s/w,u=a+(1-s/w)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="right",e.textBaseline="middle",e.fillText(U.tick(L),c-4,u)}v.unitLabel&&Ae(e,v.unitLabel,c+m/2,a+d+24,t.textMuted),U.unitLabel&&(e.save(),e.font="10px system-ui, sans-serif",e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.translate(6,a+d/2),e.rotate(-Math.PI/2),e.fillText(`(${U.unitLabel})`,0,0),e.restore());const H=b.size===0?g:g.filter(s=>b.has(G(s))),te=H.length>5e3?Math.ceil(H.length/5e3):1;e.globalAlpha=Math.max(.15,Math.min(.7,200/(H.length/te)));for(let s=0;s<H.length;s+=te){const S=H[s],r=c+(S.x-T)/n*m,L=a+(1-(S.y-k)/o)*d;e.beginPath(),e.arc(r,L,2.5,0,Math.PI*2),e.fillStyle=Q(G(S)),e.fill()}if(e.globalAlpha=1,x!==null&&f!==null){const s=$(x),S=$(f);e.strokeStyle=t.textMuted,e.fillStyle=t.textMuted,e.lineWidth=1,e.setLineDash([4,3]),e.font="9px system-ui, sans-serif",e.textBaseline="top";for(const[r,L]of[[s.limitLow,"LSL"],[s.limitHigh,"USL"]]){if(r===void 0||r<T||r>I)continue;const u=c+(r-T)/n*m;e.beginPath(),e.moveTo(u,a),e.lineTo(u,a+d),e.stroke(),e.textAlign="left",e.fillText(L,u+2,a+2)}for(const[r,L]of[[S.limitLow,"LSL"],[S.limitHigh,"USL"]]){if(r===void 0||r<k||r>N)continue;const u=a+(1-(r-k)/o)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="left",e.fillText(L,c+3,u+2)}e.setLineDash([])}e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,a),e.lineTo(c,a+d),e.moveTo(c,a+d),e.lineTo(c+m,a+d),e.stroke()}function E(){if(J(),R.length<2||x===null||f===null){g=[],ee([]),z();return}g=y?ye(P,x,f):be(q(),x,f);let t;if(y){const n=new Set(g.map(G));t=Y.filter(o=>n.has(o))}else t=Array.from(new Set(g.map(n=>n.hbin??0))).sort((n,o)=>n-o).map(String);if(ee(t),g.length>0){const n=g.map(v=>v.x),o=g.map(v=>v.y),l=Math.min(...n),p=Math.max(...n),m=Math.min(...o),d=Math.max(...o),e=(p-l)*.05||1,w=(d-m)*.05||1;T=l-e,I=p+e,k=m-w,N=d+w}z()}const ue=Me(i,()=>z());E();function fe(t,n){x=t,f=n,re.value=String(t),de.value=String(n),E()}return{card:i,setXY:fe,destroy:()=>ue.disconnect()}}
1
+ import{getColorScheme as ge}from"../../renderer/colorSchemes.js";import{categorical as be}from"./palette.js";import{buildScatterData as ye,buildScatterDataGrouped as we}from"../../stats/scatter.js";import{buildFacetTable as Se}from"../../stats/facets.js";import{CLR as T}from"../toolbar.js";import{cardShell as Te,observeResize as Me,makeTooltip as ke,attachChartTip as ve,makeTestSelect as Le,makeWaferSelect as Ce,chartFillHeight as Ae,applyCanvasFlow as De,drawAxisUnit as Be,resolveChartCanvasColors as Oe,makeAxisFormat as ne}from"./chartShell.js";const c=52,Ee=16,a=16,He=44;export function renderScatterPanel(W){const{title:oe="Test scatter",items:D,testDefs:_,colorScheme:ie="default",onSaveImage:le,groups:P}=W,{card:i,body:C,controlsRow:X}=Te(oe,le,W.ownerDocument),R=_.filter(t=>t.testNumber!==void 0),y=!!(P&&P.length>0),Y=(P??[]).map(t=>t.key),ae=new Map(Y.map((t,n)=>[t,n]));let x=W.xTestNumber??R[0]?.testNumber??null,f=W.yTestNumber??R[1]?.testNumber??x,B=null;const q=()=>B!==null?D[B]?[D[B]]:[]:D;function K(t,n,o){const l=i.ownerDocument.createElement("label");Object.assign(l.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:T.label});const p=i.ownerDocument.createElement("span");p.textContent=t;const m=Le(R,n,o,{maxWidth:"180px",emptyText:"No tests",ownerDocument:i.ownerDocument});return l.append(p,m),{wrap:l,select:m}}const{wrap:se,select:re}=K("X:",x,t=>{x=t,E()}),{wrap:ce,select:de}=K("Y:",f,t=>{f=t,E()});X.append(se,ce),!y&&D.length>1&&X.appendChild(Ce(D,B,t=>{B=t,E()},{ownerDocument:i.ownerDocument}));const O=i.ownerDocument.createElement("div");Object.assign(O.style,{color:T.warnText,background:T.warnBg,border:`1px solid ${T.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",marginBottom:"4px",display:"none"}),i.insertBefore(O,C);function J(){const t=y?[]:Se(q(),{facetableOnly:!0}).filter(n=>n.splittable).map(n=>n.label);t.length>0?(O.textContent=`\u26A0 Mixed ${t.join(", ")} across these points \u2014 trends here may be confounded (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,O.style.display=""):O.style.display="none"}J();const j=i.ownerDocument.createElement("div");j.textContent=y?"One point per die \xB7 coloured by group \xB7 click legend to filter":"One point per die across all wafers \xB7 coloured by hard bin \xB7 click legend to filter",Object.assign(j.style,{color:T.label,fontSize:"11px",marginBottom:"4px"}),i.insertBefore(j,C);const pe=ge(ie),{forBin:me}=pe,G=t=>y?t.group??"\u2014":String(t.hbin??0),Q=t=>y?be(ae.get(t)??0):me(Number(t)),V=t=>y?t:t==="0"?"No bin data":`Bin ${t}`,b=new Set,A=i.ownerDocument.createElement("div");Object.assign(A.style,{display:"flex",flexWrap:"wrap",gap:"4px",marginBottom:"6px"}),C.appendChild(A);const h=i.ownerDocument.createElement("canvas");h.style.display="block",h.style.cursor="crosshair",C.appendChild(h);const F=window.devicePixelRatio||1,xe=ke(i);let g=[],M=0,I=1,k=0,N=1;function $(t){const n=_.find(o=>o.testNumber===t);return{unit:n?.unit,limitLow:n?.limitLow,limitHigh:n?.limitHigh}}function ue(){const t=C.clientWidth,n=Math.max(200,Math.min(400,t*.65)),o=Ae(i,C,h,n);return{w:t,h:o,plotW:Math.max(10,t-c-Ee),plotH:Math.max(10,o-a-He)}}function Z(){for(const t of A.querySelectorAll("[data-cat]")){const n=t.dataset.cat,o=b.size===0||b.has(n);t.style.opacity=o?"1":"0.35";const l=b.has(n);t.style.boxShadow=l?`0 0 0 2px ${T.text}`:"none",t.setAttribute("aria-pressed",l?"true":"false")}}function ee(t){A.innerHTML="",b.clear();for(const n of t){const o=i.ownerDocument.createElement("button");o.type="button",o.dataset.cat=n,ve(o,i,xe,`${V(n)} \u2014 click to filter`);const l=Q(n);Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:"4px",padding:"2px 7px",borderRadius:"10px",border:`1px solid ${T.menuBorder}`,background:"none",cursor:"pointer",fontSize:"11px",color:T.text,whiteSpace:"nowrap"});const p=i.ownerDocument.createElement("span");Object.assign(p.style,{display:"inline-block",width:"9px",height:"9px",borderRadius:"50%",background:l,flexShrink:"0"}),o.append(p,i.ownerDocument.createTextNode(V(n))),o.addEventListener("click",()=>{b.has(n)?b.delete(n):b.add(n),Z(),z()}),A.appendChild(o)}Z()}function z(){De(h,A.offsetHeight);const t=Oe(i),n=I-M,o=N-k,{w:l,h:p,plotW:m,plotH:d}=ue();h.width=Math.max(1,Math.floor(l*F)),h.height=Math.max(1,Math.floor(p*F)),h.style.width=`${l}px`,h.style.height=`${p}px`;const e=h.getContext("2d");if(e.setTransform(F,0,0,F,0,0),e.clearRect(0,0,l,p),g.length===0){e.font="12px system-ui, sans-serif",e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.fillText("No data \u2014 select two parametric tests with values.",l/2,p/2);return}const w=4,v=ne(Math.max(Math.abs(M),Math.abs(I)),x!==null?$(x).unit:void 0),U=ne(Math.max(Math.abs(k),Math.abs(N)),f!==null?$(f).unit:void 0);e.font="10px system-ui, sans-serif",e.strokeStyle=t.border,e.lineWidth=.5,e.fillStyle=t.textMuted;for(let s=0;s<=w;s++){const S=M+n*s/w,r=c+s/w*m;e.beginPath(),e.moveTo(r,a),e.lineTo(r,a+d),e.stroke(),e.textAlign="center",e.textBaseline="top",e.fillText(v.tick(S),r,a+d+4);const L=k+o*s/w,u=a+(1-s/w)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="right",e.textBaseline="middle",e.fillText(U.tick(L),c-4,u)}v.unitLabel&&Be(e,v.unitLabel,c+m/2,a+d+24,t.textMuted),U.unitLabel&&(e.save(),e.font="10px system-ui, sans-serif",e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="middle",e.translate(6,a+d/2),e.rotate(-Math.PI/2),e.fillText(`(${U.unitLabel})`,0,0),e.restore());const H=b.size===0?g:g.filter(s=>b.has(G(s))),te=H.length>5e3?Math.ceil(H.length/5e3):1;e.globalAlpha=Math.max(.15,Math.min(.7,200/(H.length/te)));for(let s=0;s<H.length;s+=te){const S=H[s],r=c+(S.x-M)/n*m,L=a+(1-(S.y-k)/o)*d;e.beginPath(),e.arc(r,L,2.5,0,Math.PI*2),e.fillStyle=Q(G(S)),e.fill()}if(e.globalAlpha=1,x!==null&&f!==null){const s=$(x),S=$(f);e.strokeStyle=t.textMuted,e.fillStyle=t.textMuted,e.lineWidth=1,e.setLineDash([4,3]),e.font="9px system-ui, sans-serif",e.textBaseline="top";for(const[r,L]of[[s.limitLow,"LSL"],[s.limitHigh,"USL"]]){if(r===void 0||r<M||r>I)continue;const u=c+(r-M)/n*m;e.beginPath(),e.moveTo(u,a),e.lineTo(u,a+d),e.stroke(),e.textAlign="left",e.fillText(L,u+2,a+2)}for(const[r,L]of[[S.limitLow,"LSL"],[S.limitHigh,"USL"]]){if(r===void 0||r<k||r>N)continue;const u=a+(1-(r-k)/o)*d;e.beginPath(),e.moveTo(c,u),e.lineTo(c+m,u),e.stroke(),e.textAlign="left",e.fillText(L,c+3,u+2)}e.setLineDash([])}e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,a),e.lineTo(c,a+d),e.moveTo(c,a+d),e.lineTo(c+m,a+d),e.stroke()}function E(){if(J(),R.length<2||x===null||f===null){g=[],ee([]),z();return}g=y?we(P,x,f):ye(q(),x,f);let t;if(y){const n=new Set(g.map(G));t=Y.filter(o=>n.has(o))}else t=Array.from(new Set(g.map(n=>n.hbin??0))).sort((n,o)=>n-o).map(String);if(ee(t),g.length>0){const n=g.map(v=>v.x),o=g.map(v=>v.y),l=Math.min(...n),p=Math.max(...n),m=Math.min(...o),d=Math.max(...o),e=(p-l)*.05||1,w=(d-m)*.05||1;M=l-e,I=p+e,k=m-w,N=d+w}z()}const fe=Me(i,()=>z());E();function he(t,n){x=t,f=n,re.value=String(t),de.value=String(n),E()}return{card:i,setXY:he,destroy:()=>fe.disconnect()}}
@@ -1,4 +1,5 @@
1
1
  import type { Die } from '../core/dies.js';
2
+ import { type Wafer } from '../core/index.js';
2
3
  import { type MetadataFieldDef, type TestDef } from '../renderer/buildWaferMap.js';
3
4
  import type { WaferMetadata } from '../core/metadata.js';
4
5
  import { type MetadataKeySelection } from '../stats/metadataColumns.js';
@@ -94,6 +95,22 @@ export interface DieListOptions extends DieListDisplayOptions {
94
95
  label: string;
95
96
  get: (die: Die) => string | undefined;
96
97
  };
98
+ /**
99
+ * Per-die wafer lookup for Ring/Quadrant classification. A single-wafer
100
+ * caller passes a constant `() => wafer`; the lot-pooled combined list
101
+ * (multiple wafers in one export) passes a per-die lookup, since a die
102
+ * carries no wafer identity of its own — see `renderLotSummaryContent`'s
103
+ * `waferByDie`. Omitted or returning `undefined` for a die (unpositioned,
104
+ * or wafer unknown) leaves that die's Ring/Quadrant cells blank. If no die
105
+ * in the export resolves a wafer, the columns are omitted entirely rather
106
+ * than shown empty.
107
+ */
108
+ getWafer?: (die: Die) => Wafer | undefined;
109
+ /** Ring count for classification — must match the value the rest of this
110
+ * wafer/lot's analysis used (`analyzeWaferMap`/`analyzeWaferLot`'s
111
+ * `ringCount`), so a die's "Ring 2" here always means the same region a
112
+ * Ring finding elsewhere names. Default `4`, matching that default. */
113
+ ringCount?: number;
97
114
  /** Wafer-level metadata for this population, e.g. `WaferMapResult.metadata`. */
98
115
  waferMetadata?: WaferMetadata;
99
116
  /** Label/order hints for die metadata columns, e.g. `WaferMapResult.metadataFields`. */
@@ -1,13 +1,13 @@
1
- import{hasPosition as H}from"../core/dies.js";import{isParametricTest as R}from"../renderer/buildWaferMap.js";import{resolveMetadataColumns as E}from"../stats/metadataColumns.js";import{CLR as o,saveTextFile as M}from"./toolbar.js";import{csvField as $}from"./summaryPanel.js";import{fmt as T}from"../renderer/fmt.js";const B=5e4,N=new WeakSet;function F(n){if(N.has(n))return;N.add(n);const s=n.createElement("style");s.textContent=`
1
+ import{hasPosition as E,isPositionedDie as H}from"../core/dies.js";import{classifyDie as M}from"../core/index.js";import{isParametricTest as T}from"../renderer/buildWaferMap.js";import{resolveMetadataColumns as B}from"../stats/metadataColumns.js";import{CLR as r,saveTextFile as F}from"./toolbar.js";import{csvField as N}from"./summaryPanel.js";import{fmt as z}from"../renderer/fmt.js";const O=5e4,L=new WeakSet;function P(n){if(L.has(n))return;L.add(n);const o=n.createElement("style");o.textContent=`
2
2
  .wmap-dielist-table { width: 100%; border-collapse: collapse; font-size: 11px; }
3
3
  .wmap-dielist-th {
4
4
  text-align: left; padding: 4px 8px; position: sticky; top: 0;
5
- background: ${o.panelBg}; color: ${o.label}; font-weight: 600;
6
- border-bottom: 1px solid ${o.menuBorder}; white-space: nowrap;
5
+ background: ${r.panelBg}; color: ${r.label}; font-weight: 600;
6
+ border-bottom: 1px solid ${r.menuBorder}; white-space: nowrap;
7
7
  }
8
8
  .wmap-dielist-td {
9
- padding: 3px 8px; color: ${o.text};
10
- border-bottom: 1px solid ${o.menuBorder}; white-space: nowrap;
9
+ padding: 3px 8px; color: ${r.text};
10
+ border-bottom: 1px solid ${r.menuBorder}; white-space: nowrap;
11
11
  }
12
- `,n.head.appendChild(s)}function i(n,s,t,l){const r=n.createElement(s);return t&&Object.assign(r.style,t),l!==void 0&&(r.textContent=l),r}function P(n){return H(n)?`(${n.x}, ${n.y})`:"\u2014"}function D(n,s){if(s?.length)return s.filter(R);const t=new Map;for(const l of n)for(const r of Object.keys(l.testValues??{})){const d=Number(r);t.has(d)||t.set(d,{testNumber:d,name:`Test ${d}`})}return[...t.values()].sort((l,r)=>l.testNumber-r.testNumber)}export function buildDieListSection(n,s,t={}){if(!n.length)return null;const l=t.ownerDocument??document;F(l);const r=D(n,s),d=t.maxRows??B,h=Math.max(0,n.length-d),k=h>0?n.slice(0,d):n,u=i(l,"div",{display:"flex",flexDirection:"column",gap:"8px",flex:"1",minHeight:"0",minWidth:"0"}),b=i(l,"div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});b.appendChild(i(l,"div",{fontSize:"10px",fontWeight:"700",letterSpacing:"0.06em",textTransform:"uppercase",color:o.label},t.title??`Die list (${n.length})`));const x=i(l,"button",{fontSize:"11px",padding:"3px 8px",borderRadius:"4px",border:`1px solid ${o.menuBorder}`,background:o.menuBg,color:o.text,cursor:"pointer"},"Export CSV");x.type="button",b.appendChild(x),u.appendChild(b),t.note&&u.appendChild(i(l,"div",{fontSize:"11px",color:o.label,lineHeight:"1.4",flexShrink:"0"},t.note));const L=[...t.extraColumn?[t.extraColumn.label]:[],"Position","Site","Hard bin","Soft bin",...r.map(e=>e.name)],{columns:j,truncatedKeys:f}=E({dies:n,waferMetadata:t.waferMetadata,metadataFields:t.metadataFields,dieKeys:t.metadataColumns,waferKeys:t.waferMetadataColumns==="none"?"none":void 0,waferPlacement:t.waferMetadataColumns??"csv",reservedLabels:L}),g=[...t.extraColumn?[{label:t.extraColumn.label,get:e=>t.extraColumn.get(e)??""}]:[],{label:"Position",get:P},{label:"Site",get:e=>e.siteNum!==void 0?String(e.siteNum):""},{label:"Hard bin",get:e=>e.hbin!==void 0?String(e.hbin):""},{label:"Soft bin",get:e=>e.sbin!==void 0?String(e.sbin):""},...r.map(e=>({label:e.name,get:a=>{const c=a.testValues?.[e.testNumber];if(c!==void 0)return T(c,e.unit);const m=a.testPass?.[e.testNumber];return m===void 0?"":m?"PASS":"FAIL"}})),...j.map(e=>({label:e.label,get:e.get,csvOnly:e.csvOnly}))],C=g.filter(e=>!e.csvOnly),w=i(l,"div",{overflow:"auto",flex:"1",minHeight:"0",minWidth:"0",border:`1px solid ${o.menuBorder}`,borderRadius:"4px",...t.maxHeight?{maxHeight:t.maxHeight}:{}}),p=l.createElement("table");p.className="wmap-dielist-table",p.setAttribute("aria-label",t.title??`Die list (${n.length} dies)`);const S=i(l,"thead"),v=i(l,"tr");for(const e of C){const a=l.createElement("th");a.className="wmap-dielist-th",a.scope="col",a.textContent=e.label,v.appendChild(a)}S.appendChild(v),p.appendChild(S);const y=i(l,"tbody");for(const e of k){const a=i(l,"tr");for(const c of C){const m=l.createElement("td");m.className="wmap-dielist-td",m.textContent=c.get(e),a.appendChild(m)}y.appendChild(a)}if(p.appendChild(y),w.appendChild(p),u.appendChild(w),h>0||f.length>0){const e=[];h>0&&e.push(`Showing the first ${d.toLocaleString()} of ${n.length.toLocaleString()} dies. The CSV export contains all ${n.length.toLocaleString()}.`),f.length>0&&e.push(`${f.length} further metadata field${f.length===1?"":"s"} not shown.`),u.appendChild(i(l,"div",{fontSize:"11px",color:o.label,lineHeight:"1.4",flexShrink:"0"},e.join(" ")))}return x.addEventListener("click",()=>{const e=[g.map(a=>$(a.label)).join(",")];for(const a of n)e.push(g.map(c=>$(c.get(a))).join(","));M(e.join(`
13
- `),t.csvFilename??"dies.csv","text/csv",t.onSaveText)}),u}
12
+ `,n.head.appendChild(o)}function d(n,o,t,l){const i=n.createElement(o);return t&&Object.assign(i.style,t),l!==void 0&&(i.textContent=l),i}function W(n){return E(n)?String(n.x):""}function A(n){return E(n)?String(n.y):""}function I(n,o){if(o?.length)return o.filter(T);const t=new Map;for(const l of n)for(const i of Object.keys(l.testValues??{})){const s=Number(i);t.has(s)||t.set(s,{testNumber:s,name:`Test ${s}`})}return[...t.values()].sort((l,i)=>l.testNumber-i.testNumber)}function V(n,o,t){if(!o)return;const l=new Map;for(const i of n){if(!H(i))continue;const s=o(i);s&&l.set(i,M(i,s,{ringCount:t}))}return l.size>0?l:void 0}export function buildDieListSection(n,o,t={}){if(!n.length)return null;const l=t.ownerDocument??document;P(l);const i=I(n,o),s=t.maxRows??O,g=Math.max(0,n.length-s),R=g>0?n.slice(0,s):n,m=d(l,"div",{display:"flex",flexDirection:"column",gap:"8px",flex:"1",minHeight:"0",minWidth:"0"}),h=d(l,"div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});h.appendChild(d(l,"div",{fontSize:"10px",fontWeight:"700",letterSpacing:"0.06em",textTransform:"uppercase",color:r.label},t.title??`Die list (${n.length})`));const b=d(l,"button",{fontSize:"11px",padding:"3px 8px",borderRadius:"4px",border:`1px solid ${r.menuBorder}`,background:r.menuBg,color:r.text,cursor:"pointer"},"Export CSV");b.type="button",h.appendChild(b),m.appendChild(h),t.note&&m.appendChild(d(l,"div",{fontSize:"11px",color:r.label,lineHeight:"1.4",flexShrink:"0"},t.note));const x=V(n,t.getWafer,t.ringCount??4),k=n.some(e=>e.edgeExcluded===!0),j=[...t.extraColumn?[t.extraColumn.label]:[],"X","Y","Ring","Quadrant","Edge excluded","Site","Hard bin","Soft bin",...i.map(e=>e.name)],{columns:D,truncatedKeys:p}=B({dies:n,waferMetadata:t.waferMetadata,metadataFields:t.metadataFields,dieKeys:t.metadataColumns,waferKeys:t.waferMetadataColumns==="none"?"none":void 0,waferPlacement:t.waferMetadataColumns??"csv",reservedLabels:j}),C=[...t.extraColumn?[{label:t.extraColumn.label,get:e=>t.extraColumn.get(e)??""}]:[],{label:"X",get:W},{label:"Y",get:A},...x?[{label:"Ring",get:e=>{const a=x.get(e);return a?String(a.ring):""}},{label:"Quadrant",get:e=>x.get(e)?.quadrant??""}]:[],...k?[{label:"Edge excluded",get:e=>e.edgeExcluded?"Yes":"No"}]:[],{label:"Site",get:e=>e.siteNum!==void 0?String(e.siteNum):""},{label:"Hard bin",get:e=>e.hbin!==void 0?String(e.hbin):""},{label:"Soft bin",get:e=>e.sbin!==void 0?String(e.sbin):""},...i.map(e=>({label:e.name,get:a=>{const c=a.testValues?.[e.testNumber];if(c!==void 0)return z(c,e.unit);const u=a.testPass?.[e.testNumber];return u===void 0?"":u?"PASS":"FAIL"}})),...D.map(e=>({label:e.label,get:e.get,csvOnly:e.csvOnly}))],w=C.filter(e=>!e.csvOnly),S=d(l,"div",{overflow:"auto",flex:"1",minHeight:"0",minWidth:"0",border:`1px solid ${r.menuBorder}`,borderRadius:"4px",...t.maxHeight?{maxHeight:t.maxHeight}:{}}),f=l.createElement("table");f.className="wmap-dielist-table",f.setAttribute("aria-label",t.title??`Die list (${n.length} dies)`);const v=d(l,"thead"),y=d(l,"tr");for(const e of w){const a=l.createElement("th");a.className="wmap-dielist-th",a.scope="col",a.textContent=e.label,y.appendChild(a)}v.appendChild(y),f.appendChild(v);const $=d(l,"tbody");for(const e of R){const a=d(l,"tr");for(const c of w){const u=l.createElement("td");u.className="wmap-dielist-td",u.textContent=c.get(e),a.appendChild(u)}$.appendChild(a)}if(f.appendChild($),S.appendChild(f),m.appendChild(S),g>0||p.length>0){const e=[];g>0&&e.push(`Showing the first ${s.toLocaleString()} of ${n.length.toLocaleString()} dies. The CSV export contains all ${n.length.toLocaleString()}.`),p.length>0&&e.push(`${p.length} further metadata field${p.length===1?"":"s"} not shown.`),m.appendChild(d(l,"div",{fontSize:"11px",color:r.label,lineHeight:"1.4",flexShrink:"0"},e.join(" ")))}return b.addEventListener("click",()=>{const e=[C.map(a=>N(a.label)).join(",")];for(const a of n)e.push(C.map(c=>N(c.get(a))).join(","));F(e.join(`
13
+ `),t.csvFilename??"dies.csv","text/csv",t.onSaveText)}),m}
@@ -13,7 +13,7 @@ export const ICONS={aggr:'<svg viewBox="0 0 24 24" width="16" height="16" fill="
13
13
  <path d="M3 12 c0 2.5 4 4 9 4 s9-1.5 9-4" />
14
14
 
15
15
  <path d="M3 17 c0 2.5 4 4 9 4 s9-1.5 9-4" />
16
- </svg>`,orient:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76" fill="currentColor" stroke="none"/></svg>',overlays:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>',palette:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m14.622 17.897-10.68-2.913"/><path d="M18.376 2.622a1 1 0 1 1 3.002 3.002L17.36 9.643a.5.5 0 0 0 0 .707l.944.944a2.41 2.41 0 0 1 0 3.408l-.944.944a.5.5 0 0 1-.707 0L8.354 7.348a.5.5 0 0 1 0-.707l.944-.944a2.41 2.41 0 0 1 3.408 0l.944.944a.5.5 0 0 0 .707 0z"/><path d="M9 8c-1.804 2.71-3.97 3.46-6.583 3.948a.507.507 0 0 0-.302.819l7.32 8.883a1 1 0 0 0 1.185.204C12.735 20.405 16 16.792 16 15"/></svg>',pan:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20"/><path d="m15 19-3 3-3-3"/><path d="m19 9 3 3-3 3"/><path d="M2 12h20"/><path d="m5 9-3 3 3 3"/><path d="m9 5 3-3 3 3"/></svg>',quadrants:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><line x1="12" y1="3" x2="12" y2="21"/><line x1="3" y1="12" x2="21" y2="12"/></svg>',reset:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>',reticle:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="6" rx="0.5"/><rect x="13" y="3" width="8" height="6" rx="0.5"/><rect x="3" y="11" width="8" height="6" rx="0.5"/><rect x="13" y="11" width="8" height="6" rx="0.5"/><rect x="3" y="19" width="8" height="2" rx="0.5" stroke-dasharray="2 1"/><rect x="13" y="19" width="8" height="2" rx="0.5" stroke-dasharray="2 1"/></svg>',rings:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5.5"/><circle cx="12" cy="12" r="2" fill="currentColor" stroke="none"/></svg>',rotateCW:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>',specRange:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4 L5 20"/><path d="M5 4 L8 4"/><path d="M5 20 L8 20"/><path d="M19 4 L19 20"/><path d="M19 4 L16 4"/><path d="M19 20 L16 20"/><path d="M5 12 L19 12" stroke-dasharray="3 2"/></svg>',wafer:`<svg
16
+ </svg>`,orient:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76" fill="currentColor" stroke="none"/></svg>',overlays:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>',palette:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m14.622 17.897-10.68-2.913"/><path d="M18.376 2.622a1 1 0 1 1 3.002 3.002L17.36 9.643a.5.5 0 0 0 0 .707l.944.944a2.41 2.41 0 0 1 0 3.408l-.944.944a.5.5 0 0 1-.707 0L8.354 7.348a.5.5 0 0 1 0-.707l.944-.944a2.41 2.41 0 0 1 3.408 0l.944.944a.5.5 0 0 0 .707 0z"/><path d="M9 8c-1.804 2.71-3.97 3.46-6.583 3.948a.507.507 0 0 0-.302.819l7.32 8.883a1 1 0 0 0 1.185.204C12.735 20.405 16 16.792 16 15"/></svg>',pan:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20"/><path d="m15 19-3 3-3-3"/><path d="m19 9 3 3-3 3"/><path d="M2 12h20"/><path d="m5 9-3 3 3 3"/><path d="m9 5 3-3 3 3"/></svg>',print:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><path d="M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"/><rect x="6" y="14" width="12" height="8" rx="1"/></svg>',quadrants:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><line x1="12" y1="3" x2="12" y2="21"/><line x1="3" y1="12" x2="21" y2="12"/></svg>',reset:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>',reticle:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="6" rx="0.5"/><rect x="13" y="3" width="8" height="6" rx="0.5"/><rect x="3" y="11" width="8" height="6" rx="0.5"/><rect x="13" y="11" width="8" height="6" rx="0.5"/><rect x="3" y="19" width="8" height="2" rx="0.5" stroke-dasharray="2 1"/><rect x="13" y="19" width="8" height="2" rx="0.5" stroke-dasharray="2 1"/></svg>',rings:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5.5"/><circle cx="12" cy="12" r="2" fill="currentColor" stroke="none"/></svg>',rotateCW:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>',specRange:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4 L5 20"/><path d="M5 4 L8 4"/><path d="M5 20 L8 20"/><path d="M19 4 L19 20"/><path d="M19 4 L16 4"/><path d="M19 20 L16 20"/><path d="M5 12 L19 12" stroke-dasharray="3 2"/></svg>',wafer:`<svg
17
17
  viewBox="0 0 24 24"
18
18
  width="16"
19
19
  height="16"
@@ -13,4 +13,8 @@ export type { InsightsOptions, InsightsView } from './insightsTab.js';
13
13
  export { setDetachWindowOpener } from './toolbar.js';
14
14
  export type { DetachWindowOpener } from './toolbar.js';
15
15
  export type { UserGuideExtension } from './toolbar.js';
16
+ export { openWaferMapGuide } from './toolbar.js';
17
+ export { ICONS } from './icons.js';
18
+ export { openReportModal } from './toolbar.js';
19
+ export type { OverlayHandle } from './toolbar.js';
16
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- export{toCanvas}from"./toCanvas.js";export{renderWaferMap}from"./renderWaferMap.js";export{renderWaferGallery}from"./renderWaferGallery.js";export{buildDieListSection}from"./dieList.js";export{collectWarnings,severityOf}from"./warnings.js";export{setDetachWindowOpener}from"./toolbar.js";
1
+ export{toCanvas}from"./toCanvas.js";export{renderWaferMap}from"./renderWaferMap.js";export{renderWaferGallery}from"./renderWaferGallery.js";export{buildDieListSection}from"./dieList.js";export{collectWarnings,severityOf}from"./warnings.js";export{setDetachWindowOpener}from"./toolbar.js";export{openWaferMapGuide}from"./toolbar.js";export{ICONS}from"./icons.js";export{openReportModal}from"./toolbar.js";
@@ -42,6 +42,19 @@ export interface GalleryOptions {
42
42
  onViewOptionsChange?: (opts: WaferViewOptions, changed: (keyof WaferViewOptions)[], category: 'preference' | 'state' | 'mixed') => void;
43
43
  /** Legend position for bin modes. Default 'default'. */
44
44
  legendPosition?: 'default' | 'compact' | 'bottom' | 'top' | 'left' | 'floating';
45
+ /**
46
+ * Draw a legend on every card as well as the lot-level one. Default false.
47
+ *
48
+ * The lot legend already names every bin and is interactive, so per-card
49
+ * legends are off by default and the cards spend that width on the wafer.
50
+ * Turn this on when per-card bin highlighting matters more than card size.
51
+ *
52
+ * Ignored — cards always keep their own — when the plot mode is `value` with
53
+ * `colorbarRangeMode: 'data'`, where each card is scaled to its own range and
54
+ * a shared legend could not describe it. The toolbar shows the toggle
55
+ * disabled with that reason rather than appearing to do nothing.
56
+ */
57
+ perCardLegend?: boolean;
45
58
  /** Padding inside each card canvas in CSS pixels. Default 6. */
46
59
  cardPadding?: number;
47
60
  /** Filename stem for the composite gallery PNG. Default 'wafer-gallery'. */
@@ -81,7 +94,7 @@ export interface GalleryOptions {
81
94
  * Options for the always-available Summary panel alongside the gallery
82
95
  * grid — a "Lot" tab with full lot-level stats (metadata, yield, bin
83
96
  * breakdown, ring/quadrant yield, test values, findings) and a combined
84
- * Report button, plus a "Wafers" tab listing every wafer that has its own
97
+ * Report button, plus a "Findings" tab listing every wafer that has its own
85
98
  * per-wafer findings (click a row to open that wafer). Independent of
86
99
  * `insights` below: this always shows/hides its own toolbar button
87
100
  * regardless of whether Insights is open, since Insights has no per-wafer