@wafertools/wafermap 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +109 -1
- package/README.md +1 -1
- package/dist/packages/canvas-adapter/charts/chartShell.d.ts +1 -0
- package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
- package/dist/packages/canvas-adapter/dieList.d.ts +83 -17
- package/dist/packages/canvas-adapter/dieList.js +13 -2
- package/dist/packages/canvas-adapter/index.d.ts +1 -1
- package/dist/packages/canvas-adapter/insightsTab.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +10 -0
- package/dist/packages/canvas-adapter/renderWaferGallery.js +2 -2
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +13 -0
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +36 -3
- package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
- package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
- package/dist/packages/canvas-adapter/userGuideHtml.js +1 -1
- package/dist/packages/canvas-adapter/version.d.ts +2 -2
- package/dist/packages/canvas-adapter/version.js +1 -1
- package/dist/packages/core/inference/pitch.js +1 -1
- package/dist/packages/core/metadata.d.ts +29 -0
- package/dist/packages/core/metadata.js +1 -1
- package/dist/packages/renderer/buildView.d.ts +8 -0
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/stats/facets.js +1 -1
- package/dist/packages/stats/index.d.ts +2 -0
- package/dist/packages/stats/index.js +1 -1
- package/dist/packages/stats/metadataColumns.d.ts +70 -0
- package/dist/packages/stats/metadataColumns.js +1 -0
- package/package.json +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to `@wafertools/wafermap` are documented here.
|
|
3
|
+
All notable changes to `@wafertools/wafermap` are documented here. For a curated,
|
|
4
|
+
plain-language summary of what's actually changed for users and adopters, see
|
|
5
|
+
[What's New](https://wafertools.github.io/whats-new/) instead — this file is the complete
|
|
6
|
+
technical record, including internal changes.
|
|
4
7
|
|
|
5
8
|
## Versioning policy
|
|
6
9
|
|
|
@@ -19,6 +22,111 @@ under `### Breaking`.
|
|
|
19
22
|
|
|
20
23
|
---
|
|
21
24
|
|
|
25
|
+
## [0.24.0] — 2026-08-24
|
|
26
|
+
|
|
27
|
+
### Breaking
|
|
28
|
+
|
|
29
|
+
- **`buildDieListSection`/`RenderOptions.dieList` now caps rendered rows at `maxRows` (default
|
|
30
|
+
`50_000`)**, where the table previously rendered every die unconditionally. This table has
|
|
31
|
+
no virtualisation, so an uncapped multi-hundred-thousand-die lot was already a genuinely
|
|
32
|
+
heavy DOM build (~1.3M elements at 266k dies) before this release added metadata columns —
|
|
33
|
+
without the cap, adding columns would have made that worst case materially worse rather than
|
|
34
|
+
strictly better. The **CSV export is never capped**; it always contains every die, and a
|
|
35
|
+
footer states the truncation explicitly whenever it applies (`"Showing the first 50,000 of
|
|
36
|
+
266,412 dies. The CSV export contains all 266,412."`). A host that needs every row on screen
|
|
37
|
+
regardless of scale can set `maxRows` to a larger value or `Infinity`.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **Metadata now reaches the die-list table and its CSV export**, and the two per-test CSVs
|
|
42
|
+
(`test-values.csv`, `functional-tests.csv`) gain wafer identity. Previously no export surface
|
|
43
|
+
in the library emitted `die.metadata` at all — only the hover tooltip and the `'metadata'`
|
|
44
|
+
plot mode did — and none of the three CSVs emitted wafer metadata either, despite the HTML
|
|
45
|
+
reports already doing so.
|
|
46
|
+
- **Die metadata** (`DieMetadata`) is on by default in `buildDieListSection`
|
|
47
|
+
(`DieListOptions.metadataColumns`, default `'auto'`) — every key found on any die,
|
|
48
|
+
deterministically ordered (`metadataFields` declaration order first, then natural sort).
|
|
49
|
+
Unlike `metadataFields` gating the `'metadata'` plot mode (a legend has a cardinality
|
|
50
|
+
limit), a table column has none, so auto-discovery here does not conflict with that rule —
|
|
51
|
+
it follows the same intent. Pass an explicit `string[]` to pin the set, or `'none'` to omit.
|
|
52
|
+
- **Wafer metadata** (`WaferMetadata`) is CSV-only by default
|
|
53
|
+
(`DieListOptions.waferMetadataColumns`, default `'csv'`) — constant down every row, so it's
|
|
54
|
+
noise on screen next to the always-visible metadata badge, but it's what makes a detached
|
|
55
|
+
CSV self-describing enough to concatenate several exports and still know which wafer each
|
|
56
|
+
row came from. Set `'both'` to also show it in the table, or `'none'` to omit it entirely.
|
|
57
|
+
- A key present on both a wafer and a die produces exactly **one** column, scope `die`,
|
|
58
|
+
carrying the die's value — the same shadowing rule the hover tooltip already applied.
|
|
59
|
+
- Column labels resolve `metadataFields[].label` → `prettyKey(key)` → the raw key, matching
|
|
60
|
+
every other column's human-readable header. A key colliding with a built-in column name
|
|
61
|
+
(e.g. a metadata key literally called `Site`) is never dropped — it becomes
|
|
62
|
+
`"Site (metadata)"`, with further deterministic fallbacks for the rare case that also
|
|
63
|
+
collides.
|
|
64
|
+
- `RenderOptions.dieList?: DieListDisplayOptions` reaches the built-in die-list view (the
|
|
65
|
+
coordinate-less map replacement, and the "+N dies without position" footer), which
|
|
66
|
+
previously had no host hook at all. The wafer metadata and `metadataFields` it needs are
|
|
67
|
+
always supplied by the library from the current build result, never from this option, so a
|
|
68
|
+
host mistake can't substitute the wrong identity data into an export.
|
|
69
|
+
- `buildTestSection`/`buildFunctionalTestSection`/`buildLotTestSection`/
|
|
70
|
+
`buildLotFunctionalSection` (internal to the Summary/Insights panels) gained a trailing
|
|
71
|
+
`CsvExportContext` — the two lot variants self-derive it from the `perWaferSummaries` they
|
|
72
|
+
already receive, so `renderWaferGallery`'s Insights tab and the lot Summary panel needed no
|
|
73
|
+
call-site changes. A mixed lot (no metadata field common to every wafer) emits no false
|
|
74
|
+
identity column at all, rather than guessing.
|
|
75
|
+
- **"View die list" — every die as a table, reachable from the Summary panel**, on both
|
|
76
|
+
`renderWaferMap` (this wafer's own dies) and `renderWaferGallery` (every wafer in the lot,
|
|
77
|
+
pooled, with a leading `Wafer` column). Deliberately **not** a new toolbar button — reached
|
|
78
|
+
as a link inside the already-open Summary panel, the same way "Summary report" opens the
|
|
79
|
+
HTML report without one either, since the toolbar already carries eleven buttons and this
|
|
80
|
+
reuses an existing entry point rather than adding a twelfth. **On by default** whenever a
|
|
81
|
+
Summary panel is reachable at all (`RenderOptions.dieList`/`GalleryOptions.dieList`, set
|
|
82
|
+
`{ enabled: false }` to hide it); opens wmap's own resizable modal, mounting
|
|
83
|
+
`buildDieListSection` with the same metadata columns and CSV export the coordinate-less
|
|
84
|
+
die-list view already has. The lot-pooled CSV's wafer-metadata columns use `commonMetadata`,
|
|
85
|
+
so a mixed lot emits no false shared identity, same as the pooled per-test CSVs above.
|
|
86
|
+
- **`prettyKey`-labelled hover tooltip metadata.** `buildHoverText` now resolves a metadata
|
|
87
|
+
key's label the same way the new table/CSV columns do (`metadataFields[].label` →
|
|
88
|
+
`prettyKey(key)`), instead of printing the raw key. Brings the tooltip in line with every
|
|
89
|
+
other metadata surface in the library rather than shipping the inconsistency for even one
|
|
90
|
+
release. `HoverTextOptions` gained `metadataFields?: MetadataFieldDef[]`.
|
|
91
|
+
- New `@wafertools/wafermap/core` exports: `metadataDisplayValue(raw)` / `metadataCategoricalValue(raw)` —
|
|
92
|
+
the single stringifier for a metadata value (display/export vs. categorical/colour-swatch
|
|
93
|
+
use), replacing four near-identical inline implementations that had drifted (`buildView.ts`'s
|
|
94
|
+
tooltip and `'metadata'`-mode read path, `summaryPanel.ts`'s `metadataEntries`, `facets.ts`'s
|
|
95
|
+
`facetValueOf`). A `String(v)`-on-an-object bug in two of those four collapsed distinct object
|
|
96
|
+
values into the indistinguishable `"[object Object]"`; all four now emit `JSON.stringify(v)`
|
|
97
|
+
instead, so genuinely different values facet and export as genuinely different values.
|
|
98
|
+
- New `@wafertools/wafermap/stats` exports: `resolveMetadataColumns(options)` and
|
|
99
|
+
`discoverDieMetadataKeys(dies, metadataFields?, limit?)` — the column resolver backing the
|
|
100
|
+
above, public because a host building its own table/export over `die.metadata` shouldn't have
|
|
101
|
+
to reimplement discovery, ordering, shadowing, and collision resolution from scratch.
|
|
102
|
+
|
|
103
|
+
## [0.23.1] — 2026-08-18
|
|
104
|
+
|
|
105
|
+
### Added
|
|
106
|
+
|
|
107
|
+
- **The Insights tab's chart cards, section grid, sub-tab bar, Group-by select, and toggle button now carry
|
|
108
|
+
stable `data-wmap-*` hooks**, for tooling (screenshot capture, e2e tests, visual regression, accessibility
|
|
109
|
+
audits) that needs to drive the tab from outside wmap's own code. Previously these nodes were bare —
|
|
110
|
+
distinguishable only by exact heading text or button `textContent`, which breaks silently on any copy
|
|
111
|
+
change. `cardShell()` sets `data-wmap-chart-card`/`data-wmap-chart-title` (the full title, so
|
|
112
|
+
e.g. "Yield by wafer" and a hypothetical "Yield by wafer (grouped)" stay unambiguous); `makeChartGridWrap()`
|
|
113
|
+
sets `data-wmap-chart-grid`; `makeLabeledSelect()` gained an `opts.hook` param (`select.dataset.wmapSelect`)
|
|
114
|
+
since the same factory also builds every per-panel "Group:" restrict dropdown and the histogram wafer
|
|
115
|
+
picker; `insightsTab.ts`'s sub-tab buttons set `data-wmap-insights-tab` to the view key
|
|
116
|
+
(`overview`/`distributions`/`correlation`) and the back button sets `data-wmap-insights-back`; the Insights
|
|
117
|
+
toggle button in both `renderWaferMap` and `renderWaferGallery` sets `data-wmap-insights-btn` — needed
|
|
118
|
+
because its `aria-label` itself toggles between `'Insights'` and `'Back to wafer/gallery view'`, so
|
|
119
|
+
`button[aria-label="Insights"]` only ever matched while closed.
|
|
120
|
+
|
|
121
|
+
### Fixed
|
|
122
|
+
|
|
123
|
+
- **Grid pitch inference no longer stretches the die aspect ratio when only one axis has real spread.** The
|
|
124
|
+
circular-wafer constraint used to fill in an unknown aspect ratio whenever nearest-neighbour spacing looked
|
|
125
|
+
square, even when one axis had only a single distinct position observed. A handful of positioned dies
|
|
126
|
+
confined to one row (or column) isn't evidence the wafer is wider than it is tall — it just reflects which
|
|
127
|
+
subset of dies happen to carry positions — so the constraint is now skipped on a degenerate axis (dropping
|
|
128
|
+
confidence to 0.3), leaving the aspect ratio at the safer 1:1 default instead of guessing.
|
|
129
|
+
|
|
22
130
|
## [0.23.0] — 2026-08-16
|
|
23
131
|
|
|
24
132
|
### Breaking
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ and stats layers.
|
|
|
74
74
|
|
|
75
75
|
## Built with wafermap
|
|
76
76
|
|
|
77
|
-
- **[tsmap](https://github.com/wafertools/tsmap)** —
|
|
77
|
+
- **[tsmap](https://github.com/wafertools/tsmap)** — desktop and browser app for loading STDF, ATDF, CSV, JSON, and Parquet wafer data
|
|
78
78
|
|
|
79
79
|
## Local preview
|
|
80
80
|
|
|
@@ -141,6 +141,7 @@ export declare function makeLabeledSelect(labelText: string, options: readonly {
|
|
|
141
141
|
label: string;
|
|
142
142
|
}[], selected: string, onChange: (value: string) => void, opts?: {
|
|
143
143
|
maxWidth?: string;
|
|
144
|
+
hook?: string;
|
|
144
145
|
}): HTMLLabelElement;
|
|
145
146
|
export declare function makeChartGridWrap(): HTMLDivElement;
|
|
146
147
|
/** Small positioned hover tooltip, matching the pattern each ported panel builds for itself. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CLR as a,Z_BASE as T,MENU_SEARCH_THRESHOLD as N,makeMenuSearchBox as M,markMenuTrigger as E,saveImageBlob as D,openReparentedModal as I}from"../toolbar.js";import{ICONS as L}from"../icons.js";import{fmt as z,fmtColorbarAxis as W}from"../../renderer/fmt.js";export function openChartExpandModal(e,n,r){const t=e.getAttribute("style")??"";if(Object.assign(e.style,{flex:"1",minHeight:"0",border:"none",borderRadius:"0"}),!I([e],{title:n,onClosed:()=>{e.setAttribute("style",t),r&&(r.style.display="flex")}})){e.setAttribute("style",t);return}r&&(r.style.display="none")}const F={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040"},O={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border"};export function resolveChartCanvasColors(e){const n=getComputedStyle(e),r={};for(const t of Object.keys(O)){const i=n.getPropertyValue(`--wmap-${O[t]}`).trim();r[t]=i||F[t]}return r}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,n){const r=new ResizeObserver(n);return r.observe(e),{disconnect:()=>r.disconnect()}}export function growCardToFitContent(e,n,r){const t=e.offsetHeight-n.clientHeight;e.style.minHeight=`${t+r}px`}export function chartFillHeight(e,n,r,t){let i=0;for(const s of Array.from(n.children))s!==r&&(i+=s.offsetHeight);return growCardToFitContent(e,n,i+t),Math.max(t,n.clientHeight-i)}export function applyCanvasFlow(e,n=0){e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${n}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:z(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,n){if(!n)return{tick:formatValue,unitLabel:""};const{tickFmt:r,axisLabel:t}=W(e,null,n);return{tick:r,unitLabel:t}}export function drawAxisUnit(e,n,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(`(${n})`,r,t),Object.assign(e,s)}export function saveCanvasPng(e,n,r){const t=document.createElement("canvas");t.width=e.width,t.height=e.height;const i=t.getContext("2d");i.fillStyle="#ffffff",i.fillRect(0,0,t.width,t.height),i.drawImage(e,0,0),t.toBlob(s=>{s&&D(s,n,r)},"image/png")}export function cardShell(e,n){const r=document.createElement("div");Object.assign(r.style,{display:"flex",flexDirection:"column",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"6px",padding:"12px",minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const t=document.createElement("div");Object.assign(t.style,{display:"flex",alignItems:"center",gap:"6px",marginBottom:"6px"});const i=document.createElement("div");i.textContent=e,Object.assign(i.style,{color:a.value,fontSize:"13px",fontWeight:"600",flex:"1"}),t.appendChild(i);const s=document.createElement("button");s.type="button",s.title="Save as PNG",s.setAttribute("aria-label","Save as PNG"),s.innerHTML=L.download,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"}),s.addEventListener("click",()=>{const p=r.querySelector("canvas");p&&saveCanvasPng(p,e,n)}),t.appendChild(s);const o=document.createElement("button");o.type="button",o.title="Expand",o.setAttribute("aria-label","Expand"),o.innerHTML=L.expand,Object.assign(o.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"}),o.addEventListener("click",()=>openChartExpandModal(r,i.textContent??e,o)),t.appendChild(o),r.appendChild(t);const c=document.createElement("div");Object.assign(c.style,{display:"flex",gap:"6px",marginBottom:"6px",flexWrap:"wrap",alignItems:"center"}),r.appendChild(c);const d=document.createElement("div");return Object.assign(d.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),r.appendChild(d),{card:r,heading:i,controlsRow:c,body:d}}export function makeSegmented(e,n,r){const t=document.createElement("div");t.setAttribute("role","radiogroup"),Object.assign(t.style,{display:"inline-flex",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",overflow:"hidden"});const i=`seg-${Math.random().toString(36).slice(2,9)}`,s=[];return e.forEach(([o,c],d)=>{const p=document.createElement("label");Object.assign(p.style,{display:"inline-flex",alignItems:"center",fontSize:"12px",padding:"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:d>0?`1px solid ${a.menuBorder}`:"none"});const l=document.createElement("input");l.type="radio",l.name=i,l.value=o,l.checked=o===n,Object.assign(l.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const g=()=>{p.style.background=l.checked?a.bgActive:a.menuBg,p.style.color=l.checked?a.iconActive:a.text};s.push(g),g(),l.addEventListener("change",()=>{if(l.checked){for(const h of s)h();r(o)}}),p.append(l,document.createTextNode(c)),t.appendChild(p)}),t}export function makeBackButton(e){const n=document.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{fontSize:"12px",padding:"3px 10px",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:a.menuBg,color:a.label,cursor:"pointer"}),n.addEventListener("click",e),n}export function renderEmptyState(e,n,r){const t=document.createElement("div");t.textContent=n,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,n,r,t={}){const{maxWidth:i="200px",emptyText:s="No parametric tests"}=t;if(e.length>N)return P(e,n,r,i);const o=document.createElement("select");if(Object.assign(o.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:i}),e.length===0){o.disabled=!0;const c=document.createElement("option");c.textContent=s,o.appendChild(c)}else{for(const c of e){const d=document.createElement("option");d.value=String(c.testNumber),d.textContent=c.name||`Test ${c.testNumber}`,c.testNumber===n&&(d.selected=!0),o.appendChild(d)}o.addEventListener("change",()=>r(Number(o.value)))}return o}function P(e,n,r,t){const i=f=>e.find(y=>y.testNumber===f)?.name||`Test ${f}`;let s=n;const o=document.createElement("button");o.type="button",Object.assign(o.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(o,!1);const c=document.createElement("span");Object.assign(c.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const d=document.createElement("span");d.textContent="\u25BE",d.style.flex="0 0 auto",o.append(c,d);const p=()=>{c.textContent=s!==null?i(s):"(none)"};p();let l=null;const g=()=>{l&&(l.remove(),l=null,E(o,!1))},h=()=>{g(),o.focus()};function j(){const f=o.getBoundingClientRect(),y=o.ownerDocument.defaultView??window,C=Math.max(f.width,220),R=320,$=Math.min(f.left,Math.max(4,(y.innerWidth??1/0)-C-4));l=document.createElement("div"),Object.assign(l.style,{position:"fixed",top:`${f.bottom+4}px`,left:`${$}px`,background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",boxShadow:"0 4px 12px rgba(0,0,0,0.15)",zIndex:T,minWidth:`${C}px`,maxHeight:`${R}px`,overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const v=[],S=()=>v.filter(u=>u.row.style.display!=="none").map(u=>u.row),k=M(u=>{for(const m of v)m.row.style.display=m.label.includes(u)?"":"none"},"Filter tests\u2026");Object.assign(k.style,{position:"sticky",top:"0",zIndex:"1",background:a.menuBg}),k.addEventListener("keydown",u=>{if(u.key==="Escape"){h();return}u.key==="ArrowDown"&&(u.preventDefault(),S()[0]?.focus())}),l.appendChild(k);for(const u of e){const m=u.name||`Test ${u.testNumber}`,x=u.testNumber===s,b=document.createElement("div");b.textContent=m,b.setAttribute("role","menuitemradio"),b.setAttribute("aria-checked",x?"true":"false"),b.tabIndex=-1,Object.assign(b.style,{padding:"6px 14px",fontSize:"12px",cursor:"pointer",color:x?a.iconActive:a.text,fontWeight:x?"700":"400",background:x?a.menuActive:"transparent",whiteSpace:"nowrap",outline:"none"});const B=()=>{x||(b.style.background=a.menuHover)},A=()=>{b.style.background=x?a.menuActive:"transparent"};b.addEventListener("mouseenter",B),b.addEventListener("mouseleave",A),b.addEventListener("focus",B),b.addEventListener("blur",A),b.addEventListener("click",H=>{H.stopPropagation(),s=u.testNumber,p(),h(),r(u.testNumber)}),l.appendChild(b),v.push({row:b,label:m.toLowerCase()})}l.addEventListener("keydown",u=>{const m=S();if(m.length===0)return;const x=m.indexOf(document.activeElement);switch(u.key){case"ArrowDown":u.preventDefault(),m[x<0||x===m.length-1?0:x+1].focus();break;case"ArrowUp":u.preventDefault(),m[x<=0?m.length-1:x-1].focus();break;case"Home":u.preventDefault(),m[0].focus();break;case"End":u.preventDefault(),m[m.length-1].focus();break;case"Enter":case" ":x>=0&&(u.preventDefault(),m[x].click());break;case"Escape":case"Tab":u.preventDefault(),h();break}}),l.setAttribute("role","menu"),l.setAttribute("aria-label","Test"),o.ownerDocument.body.appendChild(l),E(o,!0)}o.addEventListener("click",f=>{if(f.stopPropagation(),l){g();return}j()});const w=f=>{if(!o.isConnected){o.ownerDocument.removeEventListener("click",w);return}l&&!l.contains(f.target)&&!o.contains(f.target)&&g()};return o.ownerDocument.addEventListener("click",w),Object.defineProperty(o,"value",{get:()=>s!==null?String(s):"",set:f=>{s=f===""?null:Number(f),p()}})}export function makeWaferSelect(e,n,r,t={}){const{maxWidth:i="160px",allLabel:s="All wafers"}=t,o="\0all",c=document.createElement("select");Object.assign(c.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:i});const d=document.createElement("option");return d.value=o,d.textContent=s,n===null&&(d.selected=!0),c.appendChild(d),e.forEach((p,l)=>{const g=document.createElement("option");g.value=String(l),g.textContent=p.label??`#${l}`,n===l&&(g.selected=!0),c.appendChild(g)}),c.addEventListener("change",()=>r(c.value===o?null:Number(c.value))),c}export function makeToggle(e,n,r){const t=document.createElement("label");Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:a.label,cursor:"pointer",userSelect:"none"});const i=document.createElement("input");return i.type="checkbox",i.checked=n,i.style.cssText="margin:0;cursor:pointer;",i.addEventListener("change",()=>r(i.checked)),t.append(i,document.createTextNode(e)),t}export function makeLabeledSelect(e,n,r,t,i={}){const{maxWidth:s="160px"}=i,o=document.createElement("label");o.textContent=e,Object.assign(o.style,{color:a.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const c=document.createElement("select");Object.assign(c.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:s});for(const d of n){const p=document.createElement("option");p.value=d.value,p.textContent=d.label,d.value===r&&(p.selected=!0),c.appendChild(p)}return c.addEventListener("change",()=>t(c.value)),o.appendChild(c),o}export function makeChartGridWrap(){const e=document.createElement("div");return Object.assign(e.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:"10px",flex:"0 0 auto"}),e}export function makeTooltip(e){const n=document.createElement("div");return Object.assign(n.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",fontFamily:"system-ui, sans-serif",color:a.text,whiteSpace:"nowrap",boxShadow:"0 2px 8px rgba(0,0,0,0.3)"}),e.appendChild(n),n}export function positionChartTooltip(e,n,r,t){const i=n.getBoundingClientRect(),s=14,o=4;e.style.left="0",e.style.top="0";const c=e.offsetWidth,d=e.offsetHeight;let p=r-i.left+s,l=t-i.top+s;p+c+o>i.width&&(p=r-i.left-s-c),l+d+o>i.height&&(l=t-i.top-s-d),p=Math.max(o,p),l=Math.max(o,l),e.style.left=`${p}px`,e.style.top=`${l}px`}
|
|
1
|
+
import{CLR as a,Z_BASE as T,MENU_SEARCH_THRESHOLD as N,makeMenuSearchBox as M,markMenuTrigger as E,saveImageBlob as D,openReparentedModal as I}from"../toolbar.js";import{ICONS as L}from"../icons.js";import{fmt as z,fmtColorbarAxis as W}from"../../renderer/fmt.js";export function openChartExpandModal(e,n,o){const t=e.getAttribute("style")??"";if(Object.assign(e.style,{flex:"1",minHeight:"0",border:"none",borderRadius:"0"}),!I([e],{title:n,onClosed:()=>{e.setAttribute("style",t),o&&(o.style.display="flex")}})){e.setAttribute("style",t);return}o&&(o.style.display="none")}const F={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040"},O={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border"};export function resolveChartCanvasColors(e){const n=getComputedStyle(e),o={};for(const t of Object.keys(O)){const i=n.getPropertyValue(`--wmap-${O[t]}`).trim();o[t]=i||F[t]}return o}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,n){const o=new ResizeObserver(n);return o.observe(e),{disconnect:()=>o.disconnect()}}export function growCardToFitContent(e,n,o){const t=e.offsetHeight-n.clientHeight;e.style.minHeight=`${t+o}px`}export function chartFillHeight(e,n,o,t){let i=0;for(const l of Array.from(n.children))l!==o&&(i+=l.offsetHeight);return growCardToFitContent(e,n,i+t),Math.max(t,n.clientHeight-i)}export function applyCanvasFlow(e,n=0){e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${n}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:z(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,n){if(!n)return{tick:formatValue,unitLabel:""};const{tickFmt:o,axisLabel:t}=W(e,null,n);return{tick:o,unitLabel:t}}export function drawAxisUnit(e,n,o,t,i){const l={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font="10px system-ui, sans-serif",e.textAlign="left",e.textBaseline="middle",e.fillStyle=i,e.fillText(`(${n})`,o,t),Object.assign(e,l)}export function saveCanvasPng(e,n,o){const t=document.createElement("canvas");t.width=e.width,t.height=e.height;const i=t.getContext("2d");i.fillStyle="#ffffff",i.fillRect(0,0,t.width,t.height),i.drawImage(e,0,0),t.toBlob(l=>{l&&D(l,n,o)},"image/png")}export function cardShell(e,n){const o=document.createElement("div");o.dataset.wmapChartCard="1",o.dataset.wmapChartTitle=e,Object.assign(o.style,{display:"flex",flexDirection:"column",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"6px",padding:"12px",minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const t=document.createElement("div");Object.assign(t.style,{display:"flex",alignItems:"center",gap:"6px",marginBottom:"6px"});const i=document.createElement("div");i.textContent=e,Object.assign(i.style,{color:a.value,fontSize:"13px",fontWeight:"600",flex:"1"}),t.appendChild(i);const l=document.createElement("button");l.type="button",l.title="Save as PNG",l.setAttribute("aria-label","Save as PNG"),l.innerHTML=L.download,Object.assign(l.style,{border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:"none",color:a.label,cursor:"pointer",width:"22px",height:"22px",lineHeight:"1",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),l.addEventListener("click",()=>{const p=o.querySelector("canvas");p&&saveCanvasPng(p,e,n)}),t.appendChild(l);const r=document.createElement("button");r.type="button",r.title="Expand",r.setAttribute("aria-label","Expand"),r.innerHTML=L.expand,Object.assign(r.style,{border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:"none",color:a.label,cursor:"pointer",width:"22px",height:"22px",lineHeight:"1",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),r.addEventListener("click",()=>openChartExpandModal(o,i.textContent??e,r)),t.appendChild(r),o.appendChild(t);const c=document.createElement("div");Object.assign(c.style,{display:"flex",gap:"6px",marginBottom:"6px",flexWrap:"wrap",alignItems:"center"}),o.appendChild(c);const d=document.createElement("div");return Object.assign(d.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),o.appendChild(d),{card:o,heading:i,controlsRow:c,body:d}}export function makeSegmented(e,n,o){const t=document.createElement("div");t.setAttribute("role","radiogroup"),Object.assign(t.style,{display:"inline-flex",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",overflow:"hidden"});const i=`seg-${Math.random().toString(36).slice(2,9)}`,l=[];return e.forEach(([r,c],d)=>{const p=document.createElement("label");Object.assign(p.style,{display:"inline-flex",alignItems:"center",fontSize:"12px",padding:"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:d>0?`1px solid ${a.menuBorder}`:"none"});const s=document.createElement("input");s.type="radio",s.name=i,s.value=r,s.checked=r===n,Object.assign(s.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const g=()=>{p.style.background=s.checked?a.bgActive:a.menuBg,p.style.color=s.checked?a.iconActive:a.text};l.push(g),g(),s.addEventListener("change",()=>{if(s.checked){for(const h of l)h();o(r)}}),p.append(s,document.createTextNode(c)),t.appendChild(p)}),t}export function makeBackButton(e){const n=document.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{fontSize:"12px",padding:"3px 10px",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:a.menuBg,color:a.label,cursor:"pointer"}),n.addEventListener("click",e),n}export function renderEmptyState(e,n,o){const t=document.createElement("div");t.textContent=n,Object.assign(t.style,{color:a.label,fontSize:"12px",padding:"8px 0"}),o&&Object.assign(t.style,o),e.appendChild(t)}export function makeTestSelect(e,n,o,t={}){const{maxWidth:i="200px",emptyText:l="No parametric tests"}=t;if(e.length>N)return P(e,n,o,i);const r=document.createElement("select");if(Object.assign(r.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:i}),e.length===0){r.disabled=!0;const c=document.createElement("option");c.textContent=l,r.appendChild(c)}else{for(const c of e){const d=document.createElement("option");d.value=String(c.testNumber),d.textContent=c.name||`Test ${c.testNumber}`,c.testNumber===n&&(d.selected=!0),r.appendChild(d)}r.addEventListener("change",()=>o(Number(r.value)))}return r}function P(e,n,o,t){const i=f=>e.find(y=>y.testNumber===f)?.name||`Test ${f}`;let l=n;const r=document.createElement("button");r.type="button",Object.assign(r.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:t,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:"6px",justifyContent:"space-between"}),E(r,!1);const c=document.createElement("span");Object.assign(c.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const d=document.createElement("span");d.textContent="\u25BE",d.style.flex="0 0 auto",r.append(c,d);const p=()=>{c.textContent=l!==null?i(l):"(none)"};p();let s=null;const g=()=>{s&&(s.remove(),s=null,E(r,!1))},h=()=>{g(),r.focus()};function j(){const f=r.getBoundingClientRect(),y=r.ownerDocument.defaultView??window,C=Math.max(f.width,220),R=320,$=Math.min(f.left,Math.max(4,(y.innerWidth??1/0)-C-4));s=document.createElement("div"),Object.assign(s.style,{position:"fixed",top:`${f.bottom+4}px`,left:`${$}px`,background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",boxShadow:"0 4px 12px rgba(0,0,0,0.15)",zIndex:T,minWidth:`${C}px`,maxHeight:`${R}px`,overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const v=[],S=()=>v.filter(u=>u.row.style.display!=="none").map(u=>u.row),k=M(u=>{for(const m of v)m.row.style.display=m.label.includes(u)?"":"none"},"Filter tests\u2026");Object.assign(k.style,{position:"sticky",top:"0",zIndex:"1",background:a.menuBg}),k.addEventListener("keydown",u=>{if(u.key==="Escape"){h();return}u.key==="ArrowDown"&&(u.preventDefault(),S()[0]?.focus())}),s.appendChild(k);for(const u of e){const m=u.name||`Test ${u.testNumber}`,x=u.testNumber===l,b=document.createElement("div");b.textContent=m,b.setAttribute("role","menuitemradio"),b.setAttribute("aria-checked",x?"true":"false"),b.tabIndex=-1,Object.assign(b.style,{padding:"6px 14px",fontSize:"12px",cursor:"pointer",color:x?a.iconActive:a.text,fontWeight:x?"700":"400",background:x?a.menuActive:"transparent",whiteSpace:"nowrap",outline:"none"});const B=()=>{x||(b.style.background=a.menuHover)},A=()=>{b.style.background=x?a.menuActive:"transparent"};b.addEventListener("mouseenter",B),b.addEventListener("mouseleave",A),b.addEventListener("focus",B),b.addEventListener("blur",A),b.addEventListener("click",H=>{H.stopPropagation(),l=u.testNumber,p(),h(),o(u.testNumber)}),s.appendChild(b),v.push({row:b,label:m.toLowerCase()})}s.addEventListener("keydown",u=>{const m=S();if(m.length===0)return;const x=m.indexOf(document.activeElement);switch(u.key){case"ArrowDown":u.preventDefault(),m[x<0||x===m.length-1?0:x+1].focus();break;case"ArrowUp":u.preventDefault(),m[x<=0?m.length-1:x-1].focus();break;case"Home":u.preventDefault(),m[0].focus();break;case"End":u.preventDefault(),m[m.length-1].focus();break;case"Enter":case" ":x>=0&&(u.preventDefault(),m[x].click());break;case"Escape":case"Tab":u.preventDefault(),h();break}}),s.setAttribute("role","menu"),s.setAttribute("aria-label","Test"),r.ownerDocument.body.appendChild(s),E(r,!0)}r.addEventListener("click",f=>{if(f.stopPropagation(),s){g();return}j()});const w=f=>{if(!r.isConnected){r.ownerDocument.removeEventListener("click",w);return}s&&!s.contains(f.target)&&!r.contains(f.target)&&g()};return r.ownerDocument.addEventListener("click",w),Object.defineProperty(r,"value",{get:()=>l!==null?String(l):"",set:f=>{l=f===""?null:Number(f),p()}})}export function makeWaferSelect(e,n,o,t={}){const{maxWidth:i="160px",allLabel:l="All wafers"}=t,r="\0all",c=document.createElement("select");Object.assign(c.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:i});const d=document.createElement("option");return d.value=r,d.textContent=l,n===null&&(d.selected=!0),c.appendChild(d),e.forEach((p,s)=>{const g=document.createElement("option");g.value=String(s),g.textContent=p.label??`#${s}`,n===s&&(g.selected=!0),c.appendChild(g)}),c.addEventListener("change",()=>o(c.value===r?null:Number(c.value))),c}export function makeToggle(e,n,o){const t=document.createElement("label");Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:a.label,cursor:"pointer",userSelect:"none"});const i=document.createElement("input");return i.type="checkbox",i.checked=n,i.style.cssText="margin:0;cursor:pointer;",i.addEventListener("change",()=>o(i.checked)),t.append(i,document.createTextNode(e)),t}export function makeLabeledSelect(e,n,o,t,i={}){const{maxWidth:l="160px",hook:r}=i,c=document.createElement("label");c.textContent=e,Object.assign(c.style,{color:a.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const d=document.createElement("select");r&&(d.dataset.wmapSelect=r),Object.assign(d.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:l});for(const p of n){const s=document.createElement("option");s.value=p.value,s.textContent=p.label,p.value===o&&(s.selected=!0),d.appendChild(s)}return d.addEventListener("change",()=>t(d.value)),c.appendChild(d),c}export function makeChartGridWrap(){const e=document.createElement("div");return e.dataset.wmapChartGrid="1",Object.assign(e.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:"10px",flex:"0 0 auto"}),e}export function makeTooltip(e){const n=document.createElement("div");return Object.assign(n.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",fontFamily:"system-ui, sans-serif",color:a.text,whiteSpace:"nowrap",boxShadow:"0 2px 8px rgba(0,0,0,0.3)"}),e.appendChild(n),n}export function positionChartTooltip(e,n,o,t){const i=n.getBoundingClientRect(),l=14,r=4;e.style.left="0",e.style.top="0";const c=e.offsetWidth,d=e.offsetHeight;let p=o-i.left+l,s=t-i.top+l;p+c+r>i.width&&(p=o-i.left-l-c),s+d+r>i.height&&(s=t-i.top-l-d),p=Math.max(r,p),s=Math.max(r,s),e.style.left=`${p}px`,e.style.top=`${s}px`}
|
|
@@ -1,24 +1,67 @@
|
|
|
1
1
|
import type { Die } from '../core/dies.js';
|
|
2
|
-
import { type TestDef } from '../renderer/buildWaferMap.js';
|
|
2
|
+
import { type MetadataFieldDef, type TestDef } from '../renderer/buildWaferMap.js';
|
|
3
|
+
import type { WaferMetadata } from '../core/metadata.js';
|
|
4
|
+
import { type MetadataKeySelection } from '../stats/metadataColumns.js';
|
|
3
5
|
import { type SaveTextHandler } from './toolbar.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/** Host hook for "Export CSV" — see `saveTextFile` (toolbar.ts). */
|
|
12
|
-
onSaveText?: SaveTextHandler;
|
|
6
|
+
/** Display preferences for the built-in die-list table. Everything here is a
|
|
7
|
+
* choice about what to show; the *data* it acts on (wafer metadata, metadata
|
|
8
|
+
* field definitions) is supplied by the library from its own build result
|
|
9
|
+
* when reached through `renderWaferMap`'s `RenderOptions.dieList` — never by
|
|
10
|
+
* the host substituting its own, so an export can't be handed the wrong
|
|
11
|
+
* identity data by mistake. */
|
|
12
|
+
export interface DieListDisplayOptions {
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Show a "View die list" link inside the Summary panel (`renderWaferMap`)
|
|
15
|
+
* / lot Summary panel (`renderWaferGallery`) that opens this wafer's — or,
|
|
16
|
+
* in the gallery, the whole lot's — dies as a table in a modal, with the
|
|
17
|
+
* same "Export CSV" button `buildDieListSection` always has. **Default
|
|
18
|
+
* `true`** — set `enabled: false` to hide it. Requires a Summary panel to
|
|
19
|
+
* be reachable at all (`RenderOptions.summaryPanel` / `GalleryOptions.summaryPanel`),
|
|
20
|
+
* since that panel is this link's only home; irrelevant otherwise.
|
|
21
|
+
*
|
|
22
|
+
* Has no effect on the coordinate-less map replacement or the "+N dies
|
|
23
|
+
* without position" footer, both of which show a die list unconditionally
|
|
24
|
+
* regardless of this flag — those exist because there is literally nothing
|
|
25
|
+
* else to show, not as an opt-in extra. The rest of this options object
|
|
26
|
+
* (column selection, `maxRows`, …) is shared by both surfaces.
|
|
17
27
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* `die.metadata` keys to show as columns. Default `'auto'` — every key
|
|
31
|
+
* present on any die, deterministically ordered. `'none'` omits them; an
|
|
32
|
+
* explicit array pins both the set and the order.
|
|
33
|
+
*
|
|
34
|
+
* Unlike `metadataFields` (which gates the `'metadata'` PLOT mode, where a
|
|
35
|
+
* key must be categorical enough for a legend), a table column has no
|
|
36
|
+
* cardinality limit — one cell per die, whatever the value — so this
|
|
37
|
+
* defaults to on. An export that silently drops host data is worse than a
|
|
38
|
+
* wide one.
|
|
39
|
+
*/
|
|
40
|
+
metadataColumns?: MetadataKeySelection;
|
|
41
|
+
/**
|
|
42
|
+
* Where wafer-level metadata (lot, wafer id, product, …) appears.
|
|
43
|
+
* - `'csv'` (default): CSV only. It is constant down every row, so on
|
|
44
|
+
* screen it is pure noise beside the metadata badge that already shows
|
|
45
|
+
* it — but a CSV travels alone, and per-row identity is what lets
|
|
46
|
+
* several exports be concatenated and pivoted later.
|
|
47
|
+
* - `'both'`: also as table columns.
|
|
48
|
+
* - `'none'`: omitted entirely.
|
|
49
|
+
*/
|
|
50
|
+
waferMetadataColumns?: 'csv' | 'both' | 'none';
|
|
51
|
+
/**
|
|
52
|
+
* Cap the number of rows **rendered** in the DOM. Default `50_000`. The CSV
|
|
53
|
+
* export is never capped — it always contains every die — and a footer
|
|
54
|
+
* states the truncation explicitly when it applies, since a silently
|
|
55
|
+
* truncated view is exactly the kind of unlabelled filtered population
|
|
56
|
+
* this library's own display rules forbid.
|
|
57
|
+
*
|
|
58
|
+
* Exists because this table has no virtualisation: each row is real DOM.
|
|
59
|
+
* At lot scale (a quarter-million dies is a realistic STDF batch) an
|
|
60
|
+
* unbounded table is tens of seconds and several hundred MB before any
|
|
61
|
+
* metadata column is added. Set `0` to skip the table entirely and offer
|
|
62
|
+
* only the CSV export.
|
|
63
|
+
*/
|
|
64
|
+
maxRows?: number;
|
|
22
65
|
/**
|
|
23
66
|
* Cap the scrolling table at this CSS length instead of letting it fill.
|
|
24
67
|
*
|
|
@@ -32,6 +75,29 @@ export interface DieListOptions {
|
|
|
32
75
|
* directly into a long document).
|
|
33
76
|
*/
|
|
34
77
|
maxHeight?: string;
|
|
78
|
+
/** CSV filename, including extension. Default: `'dies.csv'`. */
|
|
79
|
+
csvFilename?: string;
|
|
80
|
+
}
|
|
81
|
+
export interface DieListOptions extends DieListDisplayOptions {
|
|
82
|
+
/** Heading text. Default: `Die list (N)`. */
|
|
83
|
+
title?: string;
|
|
84
|
+
/** Optional note shown under the heading, e.g. explaining why this replaced the map. */
|
|
85
|
+
note?: string;
|
|
86
|
+
/** Host hook for "Export CSV" — see `saveTextFile` (toolbar.ts). */
|
|
87
|
+
onSaveText?: SaveTextHandler;
|
|
88
|
+
/**
|
|
89
|
+
* Extra leading column, e.g. a wafer-id label for a lot-level combined
|
|
90
|
+
* list spanning multiple wafers. `get(die)` returning `undefined` renders
|
|
91
|
+
* an empty cell for that row.
|
|
92
|
+
*/
|
|
93
|
+
extraColumn?: {
|
|
94
|
+
label: string;
|
|
95
|
+
get: (die: Die) => string | undefined;
|
|
96
|
+
};
|
|
97
|
+
/** Wafer-level metadata for this population, e.g. `WaferMapResult.metadata`. */
|
|
98
|
+
waferMetadata?: WaferMetadata;
|
|
99
|
+
/** Label/order hints for die metadata columns, e.g. `WaferMapResult.metadataFields`. */
|
|
100
|
+
metadataFields?: MetadataFieldDef[];
|
|
35
101
|
}
|
|
36
102
|
/**
|
|
37
103
|
* Build a scrollable die-list table + "Export CSV" button. Returns `null`
|
|
@@ -1,2 +1,13 @@
|
|
|
1
|
-
import{hasPosition as
|
|
2
|
-
|
|
1
|
+
import{hasPosition as k}from"../core/dies.js";import{isParametricTest as H}from"../renderer/buildWaferMap.js";import{resolveMetadataColumns as R}from"../stats/metadataColumns.js";import{CLR as a,saveTextFile as E}from"./toolbar.js";import{csvField as y}from"./summaryPanel.js";import{fmt as M}from"../renderer/fmt.js";const T=5e4;let $=!1;function B(){if($)return;$=!0;const n=document.createElement("style");n.textContent=`
|
|
2
|
+
.wmap-dielist-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
|
3
|
+
.wmap-dielist-th {
|
|
4
|
+
text-align: left; padding: 4px 8px; position: sticky; top: 0;
|
|
5
|
+
background: ${a.panelBg}; color: ${a.label}; font-weight: 600;
|
|
6
|
+
border-bottom: 1px solid ${a.menuBorder}; white-space: nowrap;
|
|
7
|
+
}
|
|
8
|
+
.wmap-dielist-td {
|
|
9
|
+
padding: 3px 8px; color: ${a.text};
|
|
10
|
+
border-bottom: 1px solid ${a.menuBorder}; white-space: nowrap;
|
|
11
|
+
}
|
|
12
|
+
`,document.head.appendChild(n)}function o(n,s,t){const i=document.createElement(n);return s&&Object.assign(i.style,s),t!==void 0&&(i.textContent=t),i}function F(n){return k(n)?`(${n.x}, ${n.y})`:"\u2014"}function P(n,s){if(s?.length)return s.filter(H);const t=new Map;for(const i of n)for(const d of Object.keys(i.testValues??{})){const r=Number(d);t.has(r)||t.set(r,{testNumber:r,name:`Test ${r}`})}return[...t.values()].sort((i,d)=>i.testNumber-d.testNumber)}export function buildDieListSection(n,s,t={}){if(!n.length)return null;B();const i=P(n,s),d=t.maxRows??T,r=Math.max(0,n.length-d),N=r>0?n.slice(0,d):n,u=o("div",{display:"flex",flexDirection:"column",gap:"8px",flex:"1",minHeight:"0"}),h=o("div",{display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:"0"});h.appendChild(o("div",{fontSize:"10px",fontWeight:"700",letterSpacing:"0.06em",textTransform:"uppercase",color:a.label},t.title??`Die list (${n.length})`));const b=o("button",{fontSize:"11px",padding:"3px 8px",borderRadius:"4px",border:`1px solid ${a.menuBorder}`,background:a.menuBg,color:a.text,cursor:"pointer"},"Export CSV");b.type="button",h.appendChild(b),u.appendChild(h),t.note&&u.appendChild(o("div",{fontSize:"11px",color:a.label,lineHeight:"1.4",flexShrink:"0"},t.note));const L=[...t.extraColumn?[t.extraColumn.label]:[],"Position","Site","Hard bin","Soft bin",...i.map(e=>e.name)],{columns:j,truncatedKeys:f}=R({dies:n,waferMetadata:t.waferMetadata,metadataFields:t.metadataFields,dieKeys:t.metadataColumns,waferKeys:t.waferMetadataColumns==="none"?"none":void 0,waferPlacement:t.waferMetadataColumns??"csv",reservedLabels:L}),x=[...t.extraColumn?[{label:t.extraColumn.label,get:e=>t.extraColumn.get(e)??""}]:[],{label:"Position",get:F},{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:l=>{const c=l.testValues?.[e.testNumber];if(c!==void 0)return M(c,e.unit);const m=l.testPass?.[e.testNumber];return m===void 0?"":m?"PASS":"FAIL"}})),...j.map(e=>({label:e.label,get:e.get,csvOnly:e.csvOnly}))],g=x.filter(e=>!e.csvOnly),C=o("div",{overflow:"auto",flex:"1",minHeight:"0",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",...t.maxHeight?{maxHeight:t.maxHeight}:{}}),p=document.createElement("table");p.className="wmap-dielist-table",p.setAttribute("aria-label",t.title??`Die list (${n.length} dies)`);const w=o("thead"),S=o("tr");for(const e of g){const l=document.createElement("th");l.className="wmap-dielist-th",l.scope="col",l.textContent=e.label,S.appendChild(l)}w.appendChild(S),p.appendChild(w);const v=o("tbody");for(const e of N){const l=o("tr");for(const c of g){const m=document.createElement("td");m.className="wmap-dielist-td",m.textContent=c.get(e),l.appendChild(m)}v.appendChild(l)}if(p.appendChild(v),C.appendChild(p),u.appendChild(C),r>0||f.length>0){const e=[];r>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(o("div",{fontSize:"11px",color:a.label,lineHeight:"1.4",flexShrink:"0"},e.join(" ")))}return b.addEventListener("click",()=>{const e=[x.map(l=>y(l.label)).join(",")];for(const l of n)e.push(x.map(c=>y(c.get(l))).join(","));E(e.join(`
|
|
13
|
+
`),t.csvFilename??"dies.csv","text/csv",t.onSaveText)}),u}
|
|
@@ -6,7 +6,7 @@ export { renderWaferGallery } from './renderWaferGallery.js';
|
|
|
6
6
|
export type { WaferMapDisplayItem, WaferMapDisplayItemFactory, GalleryOptions, GalleryController } from './renderWaferGallery.js';
|
|
7
7
|
export type { SummaryPanelOptions } from './summaryPanel.js';
|
|
8
8
|
export { buildDieListSection } from './dieList.js';
|
|
9
|
-
export type { DieListOptions } from './dieList.js';
|
|
9
|
+
export type { DieListOptions, DieListDisplayOptions } from './dieList.js';
|
|
10
10
|
export { collectWarnings, severityOf } from './warnings.js';
|
|
11
11
|
export type { WarningsOptions } from './warnings.js';
|
|
12
12
|
export type { InsightsOptions, InsightsView } from './insightsTab.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildFacetTable as de,facetValueOf as ce,FACET_NONE_VALUE as pe}from"../stats/facets.js";import{isParametricTest as ue}from"../renderer/buildWaferMap.js";import{getColorScheme as me}from"../renderer/colorSchemes.js";import{CLR as
|
|
1
|
+
import{buildFacetTable as de,facetValueOf as ce,FACET_NONE_VALUE as pe}from"../stats/facets.js";import{isParametricTest as ue}from"../renderer/buildWaferMap.js";import{getColorScheme as me}from"../renderer/colorSchemes.js";import{CLR as C}from"./toolbar.js";import{renderCapabilityPanel as fe}from"./charts/capability.js";import{renderBoxplotPanel as be}from"./charts/boxplot.js";import{renderHistogramPanel as ye}from"./charts/histogram.js";import{renderCorrelationPanel as he}from"./charts/correlation.js";import{renderScatterPanel as ge}from"./charts/scatter.js";import{renderBarPanel as q}from"./charts/barPanel.js";import{renderBinClusterPanel as Ce}from"./charts/binCluster.js";import{QUANTITY as xe}from"./charts/palette.js";import{makeChartGridWrap as j,makeLabeledSelect as Se}from"./charts/chartShell.js";import{buildYieldData as A,buildYieldDataCombined as we}from"../stats/yield.js";import{buildBinParetoData as Be}from"../stats/binPareto.js";import{buildLotTestSection as ke,buildLotFunctionalSection as ve,buildMetadataStripBox as Te}from"./summaryPanel.js";import{buildRegionYieldData as N,buildRingRegions as $e,buildQuadrantRegions as Ee}from"../stats/regions.js";import{renderRegionYieldDiagram as z}from"./charts/regionYieldDiagram.js";const Oe=[{key:"overview",label:"Overview"},{key:"distributions",label:"Distributions"},{key:"correlation",label:"Correlation"}];export function createInsightsTab(v){const{getItems:H,getLotStats:Q,getColorSchemeName:h,passBins:f,getRingCount:K,onSaveImage:y,onSaveText:M,openWafer:D}=v,U=v.showMetadataStrip??!0,S=document.createElement("div");Object.assign(S.style,{display:"none",flexDirection:"column",gap:"10px",width:"100%"});const L=document.createElement("div"),x=document.createElement("div");Object.assign(x.style,{display:"flex",gap:"4px",borderBottom:`1px solid ${C.menuBorder}`,marginBottom:"2px"}),x.setAttribute("role","tablist"),x.addEventListener("keydown",e=>{if(e.key!=="ArrowLeft"&&e.key!=="ArrowRight")return;const t=Array.from(x.querySelectorAll('[role="tab"]')),a=t.indexOf(document.activeElement);if(a===-1)return;e.preventDefault();const r=t[(a+(e.key==="ArrowRight"?1:-1)+t.length)%t.length];r.focus(),r.click()});const T=document.createElement("div");Object.assign(T.style,{display:"flex",flexDirection:"column",gap:"10px"}),S.appendChild(L),S.appendChild(x),S.appendChild(T);let $=v.defaultView??"overview",w,O=[];function V(e,t,a){D?.(e,t,a)}function _(){return H().map((e,t)=>e==null?null:{metadata:e.wafer.metadata??void 0,dies:e.dies,wafer:e.wafer,label:e.label??String(e.wafer.metadata?.waferId??""),waferIndex:t,statsSummary:e.statsSummary}).filter(e=>e!=null)}function F(e,t){Object.assign(e.style,{background:"none",border:"none",borderBottom:t?`2px solid ${C.iconActive}`:"2px solid transparent",color:t?C.iconActive:C.label,fontWeight:t?"700":"500",fontSize:"12px",padding:"6px 10px 8px",cursor:"pointer",marginBottom:"-1px"})}function X(e,t){const a=document.createElement("button");a.type="button",a.textContent=t;const r=e===$;return F(a,r),a.dataset.wmapInsightsTab=e,a.setAttribute("role","tab"),a.setAttribute("aria-selected",r?"true":"false"),a.tabIndex=r?0:-1,a.addEventListener("click",()=>{$!==e&&($=e,R())}),a}function J(e){const t=document.createElement("button");return t.type="button",t.textContent=`\u2039 ${e.label}`,F(t,!1),t.dataset.wmapInsightsBack="1",t.addEventListener("click",e.onBack),t}function Z(e,t,a,r=!0){const l=j(),s=me(h());let p="label";const d=a??"group",n=f.length===1?`bin ${f[0]}`:`bins ${f.join(", ")}`,u=Q?.(),m=new Map(u?.lotYieldSeries.map(o=>[o.waferIndex,o.yieldPercent])),c=o=>({...o,label:o.label.replace(/\s*·\s*\d+(\.\d+)?%$/,""),yieldPercent:m.get(o.waferIndex),key:o.waferIndex}),B=e.map(c),g=t?.map(o=>({key:o.key,items:o.items.map(c)})),k=()=>g?we(g,f,p):A(B,f,p),i={title:t?`Yield by ${d} (pass: ${n})`:`Yield by wafer (pass: ${n})`,data:k(),selfControl:{current:p,options:[["yield","Sort: yield"],["label",t?`Sort: ${d}`:"Sort: wafer ID"]],onChange:o=>(p=o,{data:k()})},barColor:()=>xe,valueLabel:o=>`${o.percent.toFixed(1)}%`,drill:g?{onOpenGroup:o=>{const P=g.find(E=>E.key===o.label)?.items??[];return{data:A(P,f,p),title:`Yield by wafer \u2014 ${d}: ${o.label} (pass: ${n})`}},onBack:()=>({data:k(),title:`Yield by ${d} (pass: ${n})`}),groupLabelText:d}:void 0,onOpen:D?o=>{typeof o.key=="number"&&V(o.key,`Wafer ${o.label}`)}:void 0};let b=null;if(r&&(b=q(i,y),b.card.style.minHeight="360px",l.appendChild(b.card)),t){const o=Ce({title:"Hard bin pareto",groups:t.map(P=>({key:P.key,items:P.items.map(E=>({dies:E.dies,hardBinCounts:E.statsSummary?.stats.hardBinCounts,softBinCounts:E.statsSummary?.stats.softBinCounts}))})),colorScheme:h(),onSaveImage:y});return o.card.style.minHeight="360px",l.appendChild(o.card),{card:l,destroy:()=>{b?.destroy(),o.destroy()}}}let I="hbin";const ie=s.forBin,le=e.map(o=>({dies:o.dies,hardBinCounts:o.statsSummary?.stats.hardBinCounts,softBinCounts:o.statsSummary?.stats.softBinCounts})),G=()=>Be(le,I),se={title:"Hard bin pareto",data:G(),selfControl:{current:I,options:[["hbin","Hard bins"],["sbin","Soft bins"]],onChange:o=>(I=o,{data:G(),title:`${I==="hbin"?"Hard":"Soft"} bin pareto`})},barColor:o=>ie(o.binCode??0)},Y=q(se,y);return Y.card.style.minHeight="360px",l.appendChild(Y.card),{card:l,destroy:()=>{b?.destroy(),Y.destroy()}}}function W(){const e=document.createElement("div");return e.dataset.wmapChartCard="1",Object.assign(e.style,{display:"flex",flexDirection:"column",gap:"10px",minWidth:"0",background:C.menuBg,border:`1px solid ${C.menuBorder}`,borderRadius:"6px",padding:"12px"}),e}function ee(e){if(L.innerHTML="",!U)return;const t=Te(e.map(a=>({metadata:a.metadata})),void 0,{facetableOnly:!1});t&&(t.style.marginBottom="0",L.appendChild(t))}function te(e,t,a){if(!e.length)return{elements:[],testValuesCard:null,functionalCard:null,destroy:()=>{}};const r=e.map(i=>i.wafer),l=e.map(i=>i.dies),s=e.flatMap(i=>i.dies),p=e.map(i=>i.statsSummary),d=p.every(i=>i!==void 0)?p:void 0,n=K?.()??4,u=[],m=[],c=N(l,r,n,f,$e);if(c.length){const i=z({title:"Ring yield",mode:"ring",rows:c,colorScheme:h(),onSaveImage:y});u.push(i.card),m.push(i.destroy)}const B=N(l,r,n,f,Ee);if(B.length){const i=z({title:"Quadrant yield",mode:"quadrant",rows:B,colorScheme:h(),onSaveImage:y});u.push(i.card),m.push(i.destroy)}let g=null;if(t.length){const i=ke(s,t,void 0,d,M);if(i){const b=W();b.appendChild(i),g=b}}let k=null;if(a.length){const i=ve(s,a,d,M);if(i){const b=W();b.appendChild(i),k=b}}return{elements:u,testValuesCard:g,functionalCard:k,destroy:()=>{for(const i of m)i()}}}function ne(e){const t=f.length===1?`bin ${f[0]}`:`bins ${f.join(", ")}`,a=A([{...e,key:e.waferIndex}],f)[0]?.percent,r=W();Object.assign(r.style,{flexDirection:"row",flexWrap:"wrap",gap:"8px"});function l(s,p){const d=document.createElement("div");Object.assign(d.style,{border:`1px solid ${C.menuBorder}`,borderRadius:"6px",padding:"8px 16px",textAlign:"center",minWidth:"110px"});const n=document.createElement("div");n.textContent=s,Object.assign(n.style,{fontSize:"20px",fontWeight:"700",color:C.value,lineHeight:"1.2"});const u=document.createElement("div");return u.textContent=p,Object.assign(u.style,{fontSize:"10px",color:C.label,marginTop:"2px"}),d.append(n,u),d}return a!==void 0&&r.appendChild(l(`${a.toFixed(1)}%`,`Yield \xB7 pass: ${t}`)),r.appendChild(l(String(e.dies.length),"Total dies")),r}function ae(e,t,a,r,l){const s=document.createElement("div");Object.assign(s.style,{display:"flex",flexDirection:"column",gap:"10px"});const p=e.length===1&&!r;p&&s.appendChild(ne(e[0]));const d=Z(e,r,l,!p);s.appendChild(d.card);const n=te(e,t,a);for(const u of n.elements)d.card.appendChild(u);return n.testValuesCard&&s.appendChild(n.testValuesCard),n.functionalCard&&s.appendChild(n.functionalCard),{card:s,destroy:()=>{d.destroy(),n.destroy()}}}function oe(e,t,a,r){const l=j(),s=c=>({...c,testStats:c.statsSummary?.stats.perTestStats}),p=e.map(s),d=a?.map(c=>({key:c.key,items:c.items.map(s)})),n=be({title:"Test value distribution",items:p,testDefs:t,groups:d,groupLabelText:r,colorScheme:h(),onSaveImage:y,onOpen:D?(c,B)=>V(c,`Wafer ${e.find(g=>g.waferIndex===c)?.label??c}`,B):void 0}),u=ye({title:"Value histogram",items:e,testDefs:t,groups:a,colorScheme:h(),onSaveImage:y}),m=fe({title:"Process capability",items:e,testDefs:t,groups:a,colorScheme:h(),onSaveImage:y,onSelectTest:c=>{n.setTest(c),u.setTest(c)}});return l.append(m.card,n.card,u.card),{card:l,destroy:()=>{m.destroy(),n.destroy(),u.destroy()}}}function re(e,t,a){const r=j(),l=ge({title:"Test scatter",items:e,testDefs:t,groups:a,colorScheme:h(),onSaveImage:y}),s=he({title:"Test correlation matrix",items:e,testDefs:t,groups:a,colorScheme:h(),onSaveImage:y,onSelectPair:(p,d)=>l.setXY(p,d)});return r.append(s.card,l.card),{card:r,destroy:()=>{s.destroy(),l.destroy()}}}function R(){for(const n of O)n.destroy();O=[],T.innerHTML="",x.innerHTML="",v.backTab&&x.appendChild(J(v.backTab));for(const n of Oe)x.appendChild(X(n.key,n.label));const e=_();ee(e);const t=de(e,{facetableOnly:!0}).filter(n=>n.splittable),a=document.createElement("div");Object.assign(a.style,{display:"flex",gap:"8px",alignItems:"center"});let r;t.length>0?(a.appendChild(Se("Group by:",[{value:"",label:"None"},...t.map(n=>({value:n.key,label:`${n.label} (${n.values.length})`}))],w??"",n=>{w=n||void 0,R()},{hook:"group-by"})),r=t.find(n=>n.key===w)?.label):w=void 0,T.appendChild(a);const l=H().find(n=>n?.testDefs?.length)?.testDefs??[],s=l.filter(ue);let p;if(w){const n=new Map,u=[];for(const m of e){const c=ce(m.metadata,w)??pe;n.has(c)||(n.set(c,[]),u.push(c)),n.get(c).push(m)}p=u.map(m=>({key:m,items:n.get(m)}))}const d=$==="overview"?ae(e,s,l,p,r):$==="distributions"?oe(e,s,p,r):re(e,s,p);O.push(d),T.appendChild(d.card)}return{el:S,render:R,destroy:()=>{for(const e of O)e.destroy();S.remove()}}}
|
|
@@ -6,6 +6,7 @@ import type { LotStatsSummary } from '../stats/types.js';
|
|
|
6
6
|
import { type WarningsOptions } from './warnings.js';
|
|
7
7
|
import type { SummaryPanelOptions } from './summaryPanel.js';
|
|
8
8
|
import { type InsightsOptions } from './insightsTab.js';
|
|
9
|
+
import type { DieListDisplayOptions } from './dieList.js';
|
|
9
10
|
/**
|
|
10
11
|
* The data and display overrides for a single gallery card.
|
|
11
12
|
*
|
|
@@ -87,6 +88,15 @@ export interface GalleryOptions {
|
|
|
87
88
|
* map for a finding to highlight against.
|
|
88
89
|
*/
|
|
89
90
|
summaryPanel?: SummaryPanelOptions;
|
|
91
|
+
/**
|
|
92
|
+
* Display preferences for the lot-wide "View die list" link inside the
|
|
93
|
+
* Summary panel — every die across every wafer, pooled, with a Wafer
|
|
94
|
+
* column and CSV export. **On by default** whenever `summaryPanel` is
|
|
95
|
+
* reachable at all, since that panel is the link's only home; pass
|
|
96
|
+
* `{ enabled: false }` to hide it. See `DieListDisplayOptions` and wmap's
|
|
97
|
+
* own coordinate-less die-list table, which this reuses.
|
|
98
|
+
*/
|
|
99
|
+
dieList?: DieListDisplayOptions;
|
|
90
100
|
/**
|
|
91
101
|
* Bin numbers treated as pass for yield calculation in the summary panel.
|
|
92
102
|
* Defaults to `[1]`. Must match the `passBins` passed to `analyzeWaferLot` / `buildWaferMap`
|