@wafertools/wafermap 0.26.0 → 0.27.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 +367 -0
- package/README.md +46 -3
- package/dist/packages/canvas-adapter/canvasTheme.d.ts +4 -0
- package/dist/packages/canvas-adapter/canvasTheme.js +1 -1
- package/dist/packages/canvas-adapter/charts/barPanel.d.ts +16 -0
- package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
- package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
- package/dist/packages/canvas-adapter/charts/boxplot.d.ts +13 -1
- package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
- package/dist/packages/canvas-adapter/charts/capability.d.ts +10 -10
- package/dist/packages/canvas-adapter/charts/capability.js +1 -1
- package/dist/packages/canvas-adapter/charts/chartShell.d.ts +337 -3
- package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
- package/dist/packages/canvas-adapter/charts/correlation.d.ts +8 -9
- package/dist/packages/canvas-adapter/charts/correlation.js +2 -1
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +50 -0
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -0
- package/dist/packages/canvas-adapter/charts/histogram.d.ts +12 -1
- package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
- package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
- package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +20 -0
- package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -0
- package/dist/packages/canvas-adapter/charts/trend.d.ts +30 -0
- package/dist/packages/canvas-adapter/charts/trend.js +1 -0
- package/dist/packages/canvas-adapter/dieList.js +7 -7
- package/dist/packages/canvas-adapter/identityHeader.d.ts +46 -0
- package/dist/packages/canvas-adapter/identityHeader.js +1 -0
- package/dist/packages/canvas-adapter/index.d.ts +1 -1
- package/dist/packages/canvas-adapter/insightsTab.d.ts +27 -3
- package/dist/packages/canvas-adapter/insightsTab.js +1 -1
- package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +15 -2
- package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -20
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +72 -17
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +219 -33
- package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
- package/dist/packages/canvas-adapter/toCanvas.d.ts +6 -0
- package/dist/packages/canvas-adapter/toCanvas.js +1 -1
- package/dist/packages/canvas-adapter/toolbar.d.ts +310 -6
- package/dist/packages/canvas-adapter/toolbar.js +2 -2
- package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
- package/dist/packages/canvas-adapter/userGuideHtml.js +98 -35
- package/dist/packages/canvas-adapter/version.d.ts +2 -2
- package/dist/packages/canvas-adapter/version.js +1 -1
- package/dist/packages/canvas-adapter/warnings.d.ts +0 -4
- package/dist/packages/canvas-adapter/warnings.js +1 -1
- package/dist/packages/core/utils.d.ts +11 -0
- package/dist/packages/core/utils.js +1 -1
- package/dist/packages/renderer/buildView.d.ts +34 -1
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.d.ts +104 -5
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/renderer/colorSchemes.js +1 -1
- package/dist/packages/stats/analyzeWaferMap.js +1 -1
- package/dist/packages/stats/binPareto.d.ts +15 -0
- package/dist/packages/stats/binPareto.js +1 -1
- package/dist/packages/stats/clusterDetection.js +1 -1
- package/dist/packages/stats/connectedComponents.d.ts +14 -0
- package/dist/packages/stats/connectedComponents.js +1 -0
- package/dist/packages/stats/correlation.d.ts +26 -0
- package/dist/packages/stats/correlation.js +1 -1
- package/dist/packages/stats/facets.js +1 -1
- package/dist/packages/stats/filterFindings.d.ts +18 -0
- package/dist/packages/stats/filterFindings.js +1 -1
- package/dist/packages/stats/findingsNarrative.js +1 -1
- package/dist/packages/stats/histogram.d.ts +19 -1
- package/dist/packages/stats/histogram.js +1 -1
- package/dist/packages/stats/index.d.ts +6 -0
- package/dist/packages/stats/index.js +1 -1
- package/dist/packages/stats/mergeTestDefs.d.ts +44 -0
- package/dist/packages/stats/mergeTestDefs.js +1 -0
- package/dist/packages/stats/metadataColumns.d.ts +20 -4
- package/dist/packages/stats/metadataColumns.js +1 -1
- package/dist/packages/stats/patternClassification.js +1 -1
- package/dist/packages/stats/renderFindingsReport.js +14 -14
- package/dist/packages/stats/renderSummaryReport.js +32 -29
- package/dist/packages/stats/reportHtml.js +38 -13
- package/dist/packages/stats/testPassRate.d.ts +85 -0
- package/dist/packages/stats/testPassRate.js +1 -0
- package/dist/packages/stats/trend.d.ts +40 -0
- package/dist/packages/stats/trend.js +1 -0
- package/dist/packages/stats/types.d.ts +117 -6
- package/llms.txt +6 -0
- package/package.json +7 -5
- package/dist/packages/canvas-adapter/metadataBadge.d.ts +0 -30
- package/dist/packages/canvas-adapter/metadataBadge.js +0 -1
package/README.md
CHANGED
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
[](https://github.com/wafertools/wafermap/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/@wafertools/wafermap)
|
|
7
7
|

|
|
8
|
-

|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
|
|
11
11
|
<img src="docs/images/hero-test-values.png" alt="wafermap demo" style="max-width:640px; display:block; margin:8px 0;" />
|
|
12
12
|
|
|
13
13
|
Browser-first wafer map visualization for semiconductor test data.
|
|
14
14
|
|
|
15
|
-
**Zero runtime dependencies
|
|
15
|
+
**Zero runtime dependencies.** Pure ES modules with TypeScript types — works in React,
|
|
16
|
+
Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~46 kB
|
|
17
|
+
min+gz; the interactive renderer is larger, and its chart suite and in-app guide are
|
|
18
|
+
loaded on demand rather than shipped up front — [measured sizes](docs/performance.md).
|
|
16
19
|
|
|
17
20
|
**[Project Portal: Docs & Interactive Demos →](https://wafertools.github.io/wafermap/)**
|
|
18
21
|
|
|
@@ -28,10 +31,23 @@ wafermap renders interactive wafer maps from semiconductor prober output. Hard b
|
|
|
28
31
|
- Geometry inference — pass full physical dimensions or raw prober step positions; die pitch, wafer diameter, and coordinate origin are resolved automatically
|
|
29
32
|
- `renderWaferMap` — interactive canvas map with toolbar, zoom/pan, tooltips, die selection, and summary panel
|
|
30
33
|
- `renderWaferGallery` — lot-level card grid with shared controls and click-to-expand
|
|
31
|
-
- Insights tab (`insights: { enabled: true }`) — an in-toolbar chart suite (yield, bin pareto, capability, boxplot, histogram, correlation, scatter) computed from the same wafer/lot data, for one wafer or the whole gallery
|
|
34
|
+
- Insights tab (`insights: { enabled: true }`) — an in-toolbar chart suite (yield, per-test pass rate, bin pareto, capability, boxplot, histogram, wafer-to-wafer trend, correlation, scatter) computed from the same wafer/lot data, for one wafer or the whole gallery
|
|
32
35
|
- `analyzeWaferMap` / `analyzeWaferLot` — spatial analysis across rings, quadrants, sectors, and reticle positions; failure cluster detection; lot trend series
|
|
33
36
|
- Pure ES modules, no server, no runtime dependencies — works in React, Svelte, Vue, plain HTML, or a Web Worker
|
|
34
37
|
|
|
38
|
+
## Before you build: you may not need to
|
|
39
|
+
|
|
40
|
+
**[tsmap](https://github.com/wafertools/tsmap)** is a finished, free, MIT-licensed
|
|
41
|
+
application built on this library. It opens STDF, ATDF, CSV, JSON and Parquet (plus
|
|
42
|
+
`.gz` and `.zip`), runs on Linux, macOS and Windows, and also runs
|
|
43
|
+
[in the browser](https://wafertools.github.io/tsmap/app/) with no install — files are
|
|
44
|
+
parsed locally and never uploaded. Plot modes, findings, the Insights charts, lot
|
|
45
|
+
galleries, splits and reports, without writing any code.
|
|
46
|
+
|
|
47
|
+
Build on wafermap instead when you need wafer maps **inside your own application**, a
|
|
48
|
+
data source tsmap doesn't read, or behaviour it doesn't offer. Otherwise try tsmap
|
|
49
|
+
first — integrating later is always open to you.
|
|
50
|
+
|
|
35
51
|
## Quick start
|
|
36
52
|
|
|
37
53
|
```bash
|
|
@@ -49,6 +65,18 @@ const result = buildWaferMap({
|
|
|
49
65
|
renderWaferMap(document.getElementById('map'), result);
|
|
50
66
|
```
|
|
51
67
|
|
|
68
|
+
**That is the whole integration.** No options are passed, and the map still renders
|
|
69
|
+
with its full toolbar — plot modes, colour schemes, zoom and pan, die tooltips, PNG
|
|
70
|
+
export. Add `analyzeWaferMap` for the findings and summary panel, swap in
|
|
71
|
+
`renderWaferGallery` for a lot, and you have covered what most projects need.
|
|
72
|
+
|
|
73
|
+
The API reference is long because it documents every option, not because you need
|
|
74
|
+
them: [tsmap](https://github.com/wafertools/tsmap), a complete cross-platform desktop
|
|
75
|
+
application built on this library, imports **10** of its ~100 exports. Read the
|
|
76
|
+
[Quick Start](https://wafertools.github.io/wafermap/quickstart/) first and treat the
|
|
77
|
+
[API reference](https://wafertools.github.io/wafermap/api/) as something to search,
|
|
78
|
+
not to read.
|
|
79
|
+
|
|
52
80
|
Note the two import paths. The renderers (`renderWaferMap`, `renderWaferGallery`,
|
|
53
81
|
`toCanvas`) live **only** at `@wafertools/wafermap/render`, so importing them from the
|
|
54
82
|
root package will fail. That keeps the root entry DOM-free — usable in Node for a
|
|
@@ -85,3 +113,18 @@ npm run dev
|
|
|
85
113
|
|
|
86
114
|
Serves the documentation site locally from `docs/`, including the
|
|
87
115
|
example pages under `docs/examples/`.
|
|
116
|
+
|
|
117
|
+
## Author and licence
|
|
118
|
+
|
|
119
|
+
Written by **Paul Robins**, after nearly four decades in semiconductor test — most
|
|
120
|
+
of it building test data analysis tools: wafer map and chart viewers, and the
|
|
121
|
+
platforms they were built on.
|
|
122
|
+
|
|
123
|
+
That work leaned on free software throughout — zlib, Tcl/Tk, SQLite, GCC — and it
|
|
124
|
+
was a good deal. wafermap is some of it going back the other way, prompted by
|
|
125
|
+
finding out that engineers doing this job today still have no decent free option
|
|
126
|
+
for wafer map analysis.
|
|
127
|
+
|
|
128
|
+
[MIT](LICENSE): free to use, modify and redistribute, including in commercial and
|
|
129
|
+
closed-source products. The only condition is that the copyright notice travels
|
|
130
|
+
with it.
|
|
@@ -12,6 +12,10 @@ export interface CanvasTheme {
|
|
|
12
12
|
axisLine: string;
|
|
13
13
|
/** Active-selection highlight (legend row, active bin). */
|
|
14
14
|
accent: string;
|
|
15
|
+
/** Row background under the pointer. Distinct from `accent`, which marks the
|
|
16
|
+
* SELECTED row — hover and selected must stay tellable apart (UI_STANDARDS).
|
|
17
|
+
*/
|
|
18
|
+
hoverRow: string;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
21
|
* Resolve the canvas chrome palette from `--wmap-*` custom properties on `el`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const t={background:"#f5f5f5",surface:"#fff",text:"#333",textMuted:"#999",axisLine:"#bbb",accent:"#1a66cc",hoverRow:"#edf0f8"};export function resolveCanvasTheme(c){if(!c||typeof getComputedStyle!="function")return{...t};const r=getComputedStyle(c),e=(o,a)=>r.getPropertyValue(`--wmap-${o}`).trim()||a;return{background:e("canvas-bg",e("surface",t.background)),surface:e("surface",t.surface),text:e("text",t.text),textMuted:e("text-muted",t.textMuted),axisLine:e("border",t.axisLine),accent:e("icon-active",t.accent),hoverRow:e("bg-hover",t.hoverRow)}}
|
|
@@ -36,6 +36,22 @@ export interface ChartPanel {
|
|
|
36
36
|
};
|
|
37
37
|
groupLabelText: string;
|
|
38
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Vertical rule drawn across the plot at `value`, with `label` printed at the
|
|
41
|
+
* top — a population reference the bars are read against.
|
|
42
|
+
*
|
|
43
|
+
* The yield chart needs it: without a reference, "is W07 bad?" has no visual
|
|
44
|
+
* answer on the chart itself, only by reading every printed percentage. The
|
|
45
|
+
* docked Summary panel's own per-wafer yield bars already mark the lot median,
|
|
46
|
+
* and this is the larger, more prominent view of that same data.
|
|
47
|
+
*
|
|
48
|
+
* Called on every draw, so a drilled-into group gets its own reference rather
|
|
49
|
+
* than the overall one.
|
|
50
|
+
*/
|
|
51
|
+
reference?: (data: ChartDatum[]) => {
|
|
52
|
+
value: number;
|
|
53
|
+
label: string;
|
|
54
|
+
} | null;
|
|
39
55
|
/**
|
|
40
56
|
* Clicking a leaf row (`itemCount === 1` — a single wafer, whether reached
|
|
41
57
|
* directly or by drilling into a group first) calls this to open that
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CLR as
|
|
1
|
+
import{SPACE as et,fontPx as nt,FONT as ot,CLR as lt}from"../toolbar.js";import{cardShell as it,formatValue as rt,observeResize as st,makeTooltip as ct,positionChartTooltip as at,makeBackButton as ft,makeSegmented as ht,growCardToFitContent as dt,resolveChartCanvasColors as ut,PADDING as f,VALUE_WIDTH as j,prepareCanvas as xt}from"./chartShell.js";const h=24,x=5,mt=15,z=110,bt=12;export function renderBarPanel(i,N){const{barColor:_,valueLabel:P}=i;let b=i.title,l=i.data;const{card:r,heading:R,controlsRow:G,body:A}=it(b,N,i.ownerDocument);if(i.selfControl){const e=i.selfControl;G.appendChild(ht(e.options,e.current,n=>U(n),r.ownerDocument))}const{drill:d}=i;let y=!1,B=null;const L=r.ownerDocument.createElement("div");Object.assign(L.style,{color:lt.label,fontSize:ot.body,marginBottom:et.sm}),r.insertBefore(L,A);function D(){const e=[];i.onOpen&&e.push("click to open this wafer"),d&&!y&&e.push(`click a ${d.groupLabelText} to see it by wafer`);const n=e.join(", or ");let o=n?`${n[0].toUpperCase()}${n.slice(1)}.`:"";const t=i.reference?.(l);t&&(o=o?`${o} Dashed line: ${t.label}.`:`Dashed line: ${t.label}.`),L.textContent=o}D();const w=r.ownerDocument.createElement("div"),v=()=>i.reference?mt:0,E=()=>f*2+v()+Math.min(l.length,bt)*(h+x);Object.assign(w.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${E()}px`,scrollbarGutter:"stable"}),A.appendChild(w);const a=r.ownerDocument.createElement("canvas");a.style.display="block",a.style.cursor="default",w.appendChild(a);const k=ct(r);let m=-1,M=Math.max(1,...l.map(e=>e.value));const I=e=>P?P(e):`${rt(e.value)} (${e.percent.toFixed(1)}%)`;function Y(e){const n=f+v()+e*(h+x),o=f+z,t=a.clientWidth-o-j-f;return{y:n,barX:o,barMaxWidth:Math.max(10,t)}}function p(){w.style.maxHeight=`${E()}px`,dt(r,A,E());const e=w.clientWidth,n=f*2+v()+l.length*(h+x),o=xt(a,r,e,n);if(!o)return;const{ctx:t}=o;t.font=`${nt(-1)}px system-ui, sans-serif`,t.textBaseline="middle";const s=ut(r);l.forEach((c,C)=>{const{y:u,barX:T,barMaxWidth:W}=Y(C);C===m&&(t.fillStyle=s.bgHover,t.fillRect(0,u-x/2,e,h+x)),t.fillStyle=s.text,t.textAlign="right";const S=c.label.length>16?`${c.label.slice(0,15)}\u2026`:c.label;t.fillText(S,f+z-8,u+h/2),t.fillStyle=s.track,t.fillRect(T,u,W,h);const $=Math.max(1,c.value/M*W);t.fillStyle=_?_(c,C):s.text,t.fillRect(T,u,$,h),t.fillStyle=s.text,t.textAlign="right",t.fillText(I(c),T+W+j,u+h/2)});const g=i.reference?.(l);if(g&&l.length){const{barX:c,barMaxWidth:C}=Y(0),u=Math.round(c+Math.max(0,Math.min(1,g.value/M))*C)+.5,T=f+v()-3,W=f+v()+l.length*(h+x)-x,S=(Q,Z,tt)=>{t.save(),t.strokeStyle=Q,t.lineWidth=Z,t.setLineDash(tt),t.beginPath(),t.moveTo(u,T),t.lineTo(u,W),t.stroke(),t.restore()};S(s.bg,3.5,[]),S(s.text,1.5,[4,3]),t.save(),t.font="600 10px system-ui, sans-serif",t.textBaseline="middle",t.textAlign="left";const $=4,O=t.measureText(g.label).width+$*2,X=13,V=f+v()-X-1;let H=u-O/2;H=Math.max(c,Math.min(H,c+C-O)),t.fillStyle=s.bg,t.strokeStyle=s.border,t.lineWidth=1,t.beginPath(),t.rect(H,V,O,X),t.fill(),t.stroke(),t.fillStyle=s.text,t.fillText(g.label,H+$,V+X/2+.5),t.restore()}}function U(e){if(!i.selfControl)return;i.selfControl.current=e;const n=i.selfControl.onChange(e);l=n.data,M=Math.max(1,...l.map(o=>o.value)),m=-1,n.title&&(b=n.title,R.textContent=b),D(),p()}function q(e){if(!d)return;const n=d.onOpenGroup(e);l=n.data,b=n.title,R.textContent=b,M=Math.max(1,...l.map(o=>o.value)),m=-1,y||(y=!0,B=ft(()=>J(),r.ownerDocument),G.appendChild(B)),D(),p()}function J(){if(!d)return;const e=d.onBack();l=e.data,b=e.title,R.textContent=b,M=Math.max(1,...l.map(n=>n.value)),m=-1,y=!1,B?.remove(),B=null,D(),p()}function F(e){const n=Math.floor((e-f+x/2)/(h+x));return n>=0&&n<l.length?n:-1}a.addEventListener("mousemove",e=>{const n=a.getBoundingClientRect(),o=F(e.clientY-n.top),t=o>=0&&d&&!y&&l[o].itemCount>1,s=o>=0&&(t||!!i.onOpen&&o>=0&&!t);if(o!==m&&(m=o,a.style.cursor=s?"pointer":"default",p()),o>=0){const g=l[o],c=t?`<br><em>click to see this ${d.groupLabelText} by wafer</em>`:i.onOpen?"<br><em>click to open this wafer</em>":"";k.innerHTML=`<strong>${g.label}</strong><br>${I(g)}${c}`,k.style.display="block",at(k,r,e.clientX,e.clientY)}else k.style.display="none"}),a.addEventListener("mouseleave",()=>{m!==-1&&(m=-1,p()),k.style.display="none"}),a.addEventListener("click",e=>{const n=a.getBoundingClientRect(),o=F(e.clientY-n.top);if(o===-1)return;const t=l[o];if(d&&!y&&t.itemCount>1){q(t);return}i.onOpen?.(t)});const K=st(r,()=>p());return p(),{card:r,destroy:()=>K.disconnect()}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildBinClusterData as
|
|
1
|
+
import{buildBinClusterData as x}from"../../stats/binPareto.js";import{SPACE as T,FONT as w,CLR as E}from"../toolbar.js";import{cardShell as H,makeTooltip as L,makeSegmented as $,renderEmptyState as D}from"./chartShell.js";import{renderGroupedBarPlot as I}from"./groupedBarPlot.js";const M=90,R=8;export function renderBinClusterPanel(s){const{groups:g,onSaveImage:f}=s;let n="hbin",c=s.title??"Hard bin pareto";const{card:o,heading:h,body:r,controlsRow:S}=H(c,f,s.ownerDocument);S.appendChild($([["hbin","Hard bins"],["sbin","Soft bins"]],n,e=>{n=e,c=`${n==="hbin"?"Hard":"Soft"} bin pareto`,h.textContent=c,d()},o.ownerDocument));const u=o.ownerDocument.createElement("div");u.textContent="One cluster per bin \xB7 a sub-bar per group",Object.assign(u.style,{color:E.label,fontSize:w.body,marginBottom:T.sm}),o.insertBefore(u,r);const y=L(o);let a=null;function d(){a?.destroy(),a=null,r.innerHTML="";const e=x(g,n);if(e.bins.length===0||e.groups.length===0){D(r,"No bin data available for the current grouping.");return}const p=e.groups,b=e.bins,B=Math.max(1,...b.flatMap(t=>t.counts));a=I(o,r,y,{rows:b.map(t=>({label:t.label,bars:t.counts.map(l=>({fraction:l/B})),trailing:`${t.total}`})),groups:p,labelWidth:M,maxVisibleRows:R,tooltipHtml:(t,l)=>{const i=b[t],m=i.counts[l],C=i.total>0?m/i.total*100:0;return`<strong>${i.label}</strong> \xB7 ${p[l]}<br>${m} dies (${C.toFixed(1)}% of bin)`}})}return d(),{card:o,destroy:()=>a?.destroy()}}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type BoxplotItem } from '../../stats/boxplot.js';
|
|
2
2
|
import type { TestDef } from '../../renderer/buildWaferMap.js';
|
|
3
|
-
import { type SaveImageHandler } from './chartShell.js';
|
|
3
|
+
import { type AxisPrefs, type SaveImageHandler } from './chartShell.js';
|
|
4
4
|
/** A `BoxplotItem` carrying enough identity for the panel to open its wafer
|
|
5
5
|
* on a leaf-row click — the tab's own item shape already has this. */
|
|
6
6
|
export type BoxplotPanelItem = BoxplotItem & {
|
|
@@ -25,9 +25,19 @@ export interface BoxplotPanelOptions {
|
|
|
25
25
|
key: string;
|
|
26
26
|
items: BoxplotPanelItem[];
|
|
27
27
|
}[];
|
|
28
|
+
/** Initial axis toggles, shared with the sibling distribution panels. */
|
|
29
|
+
axisPrefs?: AxisPrefs;
|
|
30
|
+
/** Fired when the user changes an axis toggle here, so siblings can follow. */
|
|
31
|
+
onAxisPrefsChange?: (prefs: AxisPrefs) => void;
|
|
32
|
+
/** Fired when the USER picks a test here, so siblings can follow. */
|
|
33
|
+
onTestChange?: (testNumber: number) => void;
|
|
28
34
|
/** Human label of the active facet (e.g. "Split"), used in the overview
|
|
29
35
|
* hint text ("click a <label>'s box to see it by wafer"). */
|
|
30
36
|
groupLabelText?: string;
|
|
37
|
+
/** Fired when the user narrows to one group here — by clicking a group's box
|
|
38
|
+
* to drill in, or Back to leave. The Insights tab owns the scope; this panel
|
|
39
|
+
* only reports the gesture. */
|
|
40
|
+
onGroupChange?: (key: string | null) => void;
|
|
31
41
|
/** Clicking a leaf row (a real per-item row — ungrouped, or drilled into
|
|
32
42
|
* a group) calls this with that item's `waferIndex` and the boxplot's
|
|
33
43
|
* currently selected `testNumber`, so the opened wafer can land on the
|
|
@@ -44,6 +54,8 @@ export interface BoxplotPanelHandle {
|
|
|
44
54
|
card: HTMLElement;
|
|
45
55
|
/** Cross-panel link (e.g. from the capability panel): switch to `testNumber` in place. */
|
|
46
56
|
setTest: (testNumber: number) => void;
|
|
57
|
+
/** Adopt the shared axis toggles (see `AxisPrefs`). */
|
|
58
|
+
setAxisPrefs: (prefs: AxisPrefs) => void;
|
|
47
59
|
destroy: () => void;
|
|
48
60
|
}
|
|
49
61
|
export declare function renderBoxplotPanel(options: BoxplotPanelOptions): BoxplotPanelHandle;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildTestBoxplotData as
|
|
1
|
+
import{buildTestBoxplotData as je}from"../../stats/boxplot.js";import{SPACE as Be,fontPx as X,FONT as Ke,CLR as Je}from"../toolbar.js";import{fmt as de}from"../../renderer/fmt.js";import{QUANTITY as De}from"./palette.js";import{cardShell as Ze,observeResize as et,makeTooltip as tt,positionChartTooltip as nt,makeBackButton as ot,makeLinkedTestSelect as it,makeToggle as lt,makeLinkedAxisPrefs as st,renderEmptyState as Ie,growCardToFitContent as at,resolveChartCanvasColors as rt,makeAxisFormat as ct,resolveAxisRange as ft,shouldIncludeLimitsByDefault as ut,drawOffAxisLimits as mt,limitLabelSide as dt,PADDING as M,VALUE_WIDTH as xt,prepareCanvas as pt}from"./chartShell.js";const k=24,L=5,Re=110,ht=12,He=20;export function renderBoxplotPanel(y){const{title:J="Test value distribution",items:Oe,testDefs:xe,onSaveImage:We,groups:m,groupLabelText:Z="group",onOpen:N}=y,{card:a,heading:Ee,body:d,controlsRow:q}=Ze(J,We,y.ownerDocument);a.style.flex="0 0 auto",d.style.flex="0 0 auto",a.style.alignSelf="start";const ee=xe.filter(n=>n.testNumber!==void 0);let v=y.selectedTestNumber??ee[0]?.testNumber??null,te=!1,Y=0,x=null,I=null,ne=null,pe=null;function Pe(){const n=!m||m.length===0?"":x??"\0overview";if(ne&&pe===n)return ne;const u=!m||m.length===0?Oe:x!==null?m.find(h=>h.key===x)?.items??[]:m.map(h=>({label:h.key,dies:h.items.flatMap(c=>c.dies??[])}));return ne=u,pe=n,u}function he(){x!==null&&!I?(I=ot(()=>{ge(null),y.onGroupChange?.(null)},a.ownerDocument),q.appendChild(I)):x===null&&I&&(I.remove(),I=null),Ee.textContent=x!==null?`${J} \u2014 ${Z}: ${x}`:J}function ge(n){return x===n||n!==null&&!(m??[]).some(u=>u.key===n)?!1:(x=n,he(),Q(),C(),!0)}const be=it(ee,v,n=>{v=n,C(),y.onTestChange?.(n)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),_e=be.el;q.appendChild(_e),q.appendChild(lt("Log scale",te,n=>{te=n,C()},a.ownerDocument));const oe=a.ownerDocument.createElement("span");Object.assign(oe.style,{display:"inline-flex",gap:Be.lg,alignItems:"center"}),q.appendChild(oe);const ie=st(oe,y.axisPrefs,n=>{y.onAxisPrefsChange?.(n),C()},a.ownerDocument),le=a.ownerDocument.createElement("div");Object.assign(le.style,{color:Je.label,fontSize:Ke.body,marginBottom:Be.sm}),a.insertBefore(le,d);function Q(){const n=!!m&&m.length>0&&x===null,u=[];n?u.push(`click a ${Z}'s box to see it by wafer`):N&&u.push("click a box to open that wafer");const h=u.length?`${u[0][0].toUpperCase()}${u[0].slice(1)} \xB7 `:"";le.textContent=`${h}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(Y?` \xB7 axis clipped, ${Y} value${Y===1?"":"s"} outside`:"")}Q(),he();const H=tt(a);let se=null;function C(){const{includeLimits:n,clipOutliers:u}=ie.get();if(Q(),d.innerHTML="",ee.length===0||v===null){Ie(d,"No parametric test data available for box plots.");return}const h=Pe(),c=je(h,v),ae=xe.find(t=>t.testNumber===v),U=ae?.unit,z=ae?.limitLow,F=ae?.limitHigh;if(c.every(t=>t.count===0)){Ie(d,"No parametric test data available for box plots.");return}const p=a.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",d.appendChild(p);const Te=M*2+Math.min(c.length,ht)*(k+L)+He;d.style.maxHeight=`${Te}px`,d.style.overflowY="auto",d.style.scrollbarGutter="stable";let O=-1;const re=c.filter(t=>t.count>0),ye=Math.min(...re.map(t=>t.min)),ve=Math.max(...re.map(t=>t.max)),we=n??ut(ye,ve,z,F);ie.sync(we,z!==void 0||F!==void 0);const W=ft({dataMin:ye,dataMax:ve,limitLow:z,limitHigh:F,includeLimits:we,clipOutliers:u,values:re.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});Y=W.clippedCount,Q();const R=W.lo,ce=W.hi,Me=ce-R||1,V=te&&R>0,fe=V?Math.log10(R):0,ke=(V?Math.log10(ce):0)-fe||1,ue=ct(Math.max(Math.abs(R),Math.abs(ce)),U);function Ne(){const t=M+Re,i=p.clientWidth-t-xt-M;return{plotX:t,plotMaxWidth:Math.max(10,i)}}function S(t,i,s){return V?i+(Math.log10(t)-fe)/ke*s:i+(t-R)/Me*s}function qe(){const t=[];for(let i=0;i<=4;i++)t.push(V?Math.pow(10,fe+ke*i/4):R+Me*i/4);return t}function j(){const t=rt(a),i=d.clientWidth,s=M*2+c.length*(k+L)+He,A=pt(p,a,i,s);if(!A)return;const{ctx:e}=A;e.font=`${X(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:f}=Ne();c.forEach((o,g)=>{const T=M+g*(k+L),r=T+k/2;g===O&&(e.fillStyle=t.bgHover,e.fillRect(0,T-L/2,i,k+L)),e.fillStyle=t.text,e.textAlign="right";const Qe=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(Qe,M+Re-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const Ue=l+f,P=Ve=>Math.min(Ue,Math.max(l,Ve)),Se=S(o.min,l,f),Ae=S(o.max,l,f),$=P(Se),_=P(S(o.q1,l,f)),$e=P(S(o.median,l,f)),me=P(S(o.q3,l,f)),B=P(Ae),ze=Se<$,Fe=Ae>B,D=T+3,G=T+k-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo($,r),e.lineTo(_,r),e.moveTo(me,r),e.lineTo(B,r);const w=3;ze?(e.moveTo($+w,r-w),e.lineTo($,r),e.lineTo($+w,r+w)):(e.moveTo($,D),e.lineTo($,G)),Fe?(e.moveTo(B-w,r-w),e.lineTo(B,r),e.lineTo(B-w,r+w)):(e.moveTo(B,D),e.lineTo(B,G)),e.stroke(),e.fillStyle=De,e.globalAlpha=.35,e.fillRect(_,D,Math.max(1,me-_),G-D),e.globalAlpha=1,e.strokeStyle=De,e.strokeRect(_,D,Math.max(1,me-_),G-D),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo($e,D),e.lineTo($e,G),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(de(o.median,U,"engineering"),l+f+10,r)});const b=M+c.length*(k+L);e.font=`${X(-1)}px system-ui, sans-serif`,mt(e,W.offAxis,{left:l,right:l+f,top:0,bottom:b},"horizontal",t.limitLine,o=>de(o,U,"engineering"));const Ye=new Set(W.offAxis.map(o=>o.value));for(const[o,g]of[[z,"LSL"],[F,"USL"]]){if(o===void 0||Ye.has(o))continue;const T=S(o,l,f);e.strokeStyle=t.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(T,0),e.lineTo(T,b),e.stroke(),e.setLineDash([]),e.fillStyle=t.limitLine;const r=dt(T,e.measureText(g).width,l,l+f,g==="LSL");e.textAlign=r<0?"right":"left",e.textBaseline="bottom",e.fillText(g,T+r*3,b-1)}e.font=`${X(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,b),e.lineTo(l+f,b),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";for(const o of qe()){const g=S(o,l,f);e.beginPath(),e.moveTo(g,b),e.lineTo(g,b+4),e.stroke(),e.fillText(ue.tick(o),g,b+6)}ue.unitLabel&&(e.textAlign="left",e.font=`${X(-1)}px system-ui, sans-serif`,e.fillText(`(${ue.unitLabel})`,l+f+22,b+6),e.font=`${X(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function Le(t){const i=Math.floor((t-M+L/2)/(k+L));return i>=0&&i<c.length?i:-1}function E(t){return de(t,U,"engineering")}const K=!!m&&m.length>0&&x===null,Ce=t=>!K&&!!N&&h[t]?.waferIndex!==void 0;p.addEventListener("mousemove",t=>{const i=p.getBoundingClientRect(),s=Le(t.clientY-i.top),A=s>=0&&c[s].count>0&&(K||Ce(s));if(s!==O&&(O=s,p.style.cursor=A?"pointer":"default",j()),s>=0&&c[s].count>0){const e=c[s],l=K?`<br><em>click to see this ${Z} by wafer</em>`:Ce(s)?"<br><em>click to open this wafer</em>":"";H.innerHTML=`<strong>${e.label}</strong> (${e.count} dies)<br>max ${E(e.max)}<br>q3 ${E(e.q3)}<br>median ${E(e.median)}<br>q1 ${E(e.q1)}<br>min ${E(e.min)}${l}`,H.style.display="block",nt(H,a,t.clientX,t.clientY)}else H.style.display="none"}),p.addEventListener("mouseleave",()=>{O!==-1&&(O=-1,j()),H.style.display="none"}),p.addEventListener("click",t=>{const i=p.getBoundingClientRect(),s=Le(t.clientY-i.top);if(s===-1||c[s].count===0)return;if(K){const e=c[s].label;ge(e),y.onGroupChange?.(e);return}const A=h[s]?.waferIndex;N&&A!==void 0&&v!==null&&N(A,v)}),se?.disconnect(),se=et(a,()=>j()),j(),at(a,d,Te)}C();function Ge(n){be.set(n)&&(v=n,C())}function Xe(n){ie.set(n)&&C()}return{card:a,setTest:Ge,setAxisPrefs:Xe,destroy:()=>se?.disconnect()}}
|
|
@@ -7,20 +7,18 @@ export interface CapabilityPanelOptions {
|
|
|
7
7
|
testDefs: TestDef[];
|
|
8
8
|
colorScheme?: string;
|
|
9
9
|
onSaveImage?: SaveImageHandler;
|
|
10
|
+
/** Test to mark as the section's current one, if any. */
|
|
11
|
+
selectedTestNumber?: number;
|
|
10
12
|
/** Clicking a test's box calls this — the Analysis tab wires it to drive the boxplot panel's selected test in place, mirroring tsmap's original capability→boxplot link. */
|
|
11
13
|
onSelectTest?: (testNumber: number) => void;
|
|
12
14
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Grouping is NOT this panel's concern. The Insights tab owns one "Show:"
|
|
16
|
+
* scope for every view and hands each panel the population it names, so a
|
|
17
|
+
* narrowed population arrives here as plain `items`. This panel previously
|
|
18
|
+
* took a `groups` list and silently restricted itself to `groups[0]` with no
|
|
19
|
+
* way back to the pooled view — a chart captioned as the lot while drawing
|
|
20
|
+
* one sixth of it.
|
|
19
21
|
*/
|
|
20
|
-
groups?: {
|
|
21
|
-
key: string;
|
|
22
|
-
items: CapabilityItem[];
|
|
23
|
-
}[];
|
|
24
22
|
/** Document to build this panel's DOM into. Default `document` — pass the
|
|
25
23
|
* host's own `ownerDocument` when the container might live in a
|
|
26
24
|
* different document (e.g. a gallery card detached into its own popup
|
|
@@ -40,6 +38,8 @@ export interface CapabilityPanelHandle {
|
|
|
40
38
|
*/
|
|
41
39
|
hasData: boolean;
|
|
42
40
|
/** Disconnect this panel's own ResizeObserver. Call when removing the card from the DOM. */
|
|
41
|
+
/** Mark the test the section is showing. Redraws only when it changes. */
|
|
42
|
+
setTest: (testNumber: number) => void;
|
|
43
43
|
destroy: () => void;
|
|
44
44
|
}
|
|
45
45
|
export declare function renderCapabilityPanel(options: CapabilityPanelOptions): CapabilityPanelHandle;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildCapabilityData as
|
|
1
|
+
import{buildCapabilityData as fe}from"../../stats/capability.js";import{capabilityColor as B}from"./palette.js";import{LEADING as ue,SPACE as g,fontPx as G,FONT as Q,CLR as H}from"../toolbar.js";import{cardShell as be,chartFillHeight as ye,applyCanvasFlow as xe,observeResize as ge,makeTooltip as ve,positionChartTooltip as Se,renderEmptyState as Me,resolveChartCanvasColors as Te,chartSwatchCss as Le,prepareCanvas as ke,chartDpr as Ce}from"./chartShell.js";import{fmt as Pe}from"../../renderer/fmt.js";const $e=30,Ae=160,ee=90,te=28,v=42;function I(C){return C===null?"\u2014":C.toFixed(2)}export function renderCapabilityPanel(C){const{title:ne="Process capability",items:oe,testDefs:z,onSaveImage:le,onSelectTest:O}=C;let X=C.selectedTestNumber??null;const{card:r,body:S}=be(ne,le,C.ownerDocument),se=z.some(n=>n.testNumber!==void 0);S.style.overflowX="auto";function ie(){return oe}const P=r.ownerDocument.createElement("div");Object.assign(P.style,{display:"flex",flexDirection:"column",gap:g.xs,marginBottom:g.sm}),r.insertBefore(P,S);const W=ve(r);let R=()=>{};function ae(n,i,f){P.innerHTML="";const c=r.ownerDocument.createElement("span");Object.assign(c.style,{display:"inline-flex",alignItems:"center",gap:g.sm,color:H.value,fontSize:Q.body,fontWeight:"500"});const M=f-n,b=i>0?` \xB7 ${i} without spec limits`:"",y=r.ownerDocument.createElement("span");y.textContent=M>0?`${n} of ${f} tests shown${b} \xB7 ${M} excluded (no recorded values)`:`${n} test${n!==1?"s":""} shown${b}`,c.appendChild(y),P.appendChild(c);const $=i>=n,T=r.ownerDocument.createElement("div");T.textContent=$?"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(T.style,{color:H.label,fontSize:Q.body,lineHeight:ue.base,marginTop:g.xs,maxWidth:"78ch"}),P.appendChild(T),P.appendChild(re(r.ownerDocument,i>0,$))}function re(n,i,f=!1){const c=n.createElement("div");Object.assign(c.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${g.xs} ${g.xl}`,marginTop:g.xl,marginBottom:g.xs,fontSize:Q.body,color:H.label});const M=f?[]:[{color:B(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:B(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:B(.5),label:"Poor \xB7 Ppk < 1.0"}];i&&M.push({color:H.label,label:"No spec limits \xB7 no Ppk",dashed:!0});for(const b of M){const y=n.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:g.sm});const $=n.createElement("span");$.style.cssText=Le(b.color,b.dashed?"outline":"solid");const T=n.createElement("span");T.textContent=b.label,y.append($,T),c.appendChild(y)}return c}function ce(n){if(S.innerHTML="",n.length===0)return Me(S,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const i=r.ownerDocument.createElement("canvas");i.style.display="block",i.style.cursor="default",S.appendChild(i);const f=n.length,c=Math.min(0,...n.map(t=>t.min)),M=Math.max(1,...n.map(t=>t.max)),b=(M-c)*.05||.1,y=c-b,T=M+b-y||1;function V(t){return Math.max($e,Math.min(Ae,Math.floor(t/f)))}function L(t,s,o){return s+(1-(t-y)/T)*o}function pe(t,s,o,l,k){const x=n.filter(a=>a.hasSpec).length,A=x===n.length,d=x===0,_=d?"max":"USL",e=d?"min":"LSL",h=A?"normalised to spec":d?"normalised to range":"normalised (per test)";t.font=`${G(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle";for(const[a,D]of[[1,_],[0,e]]){const p=L(a,o,l);p<o-1||p>o+l+1||(t.strokeStyle=s.limitLine,t.lineWidth=1,t.beginPath(),t.moveTo(v-5,p),t.lineTo(v,p),t.stroke(),t.fillStyle=s.textMuted,t.fillText(D,v-8,p))}t.save(),t.translate(9,o+l/2),t.rotate(-Math.PI/2),t.textAlign="center",t.fillStyle=s.textMuted,t.fillText(h,0,0),t.restore()}let w=-1;function j(){xe(i);const t=S.clientWidth,s=V(t-v),o=s*f,l=te,k=v+o,x=ye(r,S,i,te+200+ee),A=Math.max(l+60,x-ee),d=A-l,_=ke(i,r,k,x);if(!_)return;const{ctx:e}=_,h=Te(r);n.forEach((a,D)=>{const p=v+D*s,m=p+s/2,u=Math.max(4,s*.55);if(D===w&&(e.fillStyle=h.bgHover,e.fillRect(p,l,s,A-l)),n[D].testNumber===X&&(e.fillStyle=h.accent,e.fillRect(p,A-2,s,2)),a.hasSpec){const K=L(0,l,d),Z=L(1,l,d);e.strokeStyle=h.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(p,K),e.lineTo(p+s,K),e.moveTo(p,Z),e.lineTo(p+s,Z),e.stroke(),e.setLineDash([])}const F=a.hasSpec?B(a.ppk):h.textMuted,U=L(a.min,l,d),E=L(a.q1,l,d),J=L(a.median,l,d),N=L(a.q3,l,d),q=L(a.max,l,d);e.strokeStyle=h.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(m,U),e.lineTo(m,E),e.moveTo(m,N),e.lineTo(m,q),e.moveTo(m-u/4,U),e.lineTo(m+u/4,U),e.moveTo(m-u/4,q),e.lineTo(m+u/4,q),e.stroke(),e.fillStyle=F,e.globalAlpha=.5,e.fillRect(m-u/2,Math.min(E,N),u,Math.max(1,Math.abs(N-E))),e.globalAlpha=1,e.strokeStyle=F,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(m-u/2,Math.min(E,N),u,Math.max(1,Math.abs(N-E))),e.setLineDash([]),e.strokeStyle=h.text,e.lineWidth=2,e.beginPath(),e.moveTo(m-u/2,J),e.lineTo(m+u/2,J),e.stroke(),e.lineWidth=1,e.font=`${G(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?F:h.textMuted,e.fillText(a.hasSpec?I(a.ppk):"\u2014",m,l-8);const he=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(m,A+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${G(-1)}px system-ui, sans-serif`,e.fillStyle=h.textMuted,e.fillText(he,0,0),e.restore()}),e.strokeStyle=h.border,e.lineWidth=1,e.strokeRect(v,l,o,d),pe(e,h,l,d,o)}function Y(t){const s=i.getBoundingClientRect(),o=S.clientWidth,l=V(o-v),k=(t.clientX-s.left)*(i.width/Ce(i)/s.width)-v,x=Math.floor(k/l);return x>=0&&x<f?x:-1}return i.addEventListener("mousemove",t=>{const s=Y(t);if(s!==w&&(w=s,j()),s===-1){W.style.display="none";return}const o=n[s],l=k=>Pe(k,o.unit);W.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)}`)+(O?"<br><em>click to view in boxplot</em>":""),W.style.display="block",Se(W,r,t.clientX,t.clientY),i.style.cursor=O?"pointer":"default"}),i.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,j()),W.style.display="none"}),i.addEventListener("click",t=>{if(!O)return;const s=Y(t);s!==-1&&O(n[s].testNumber)}),j}function me(){const n=fe(ie(),z),i=z.filter(c=>c.testNumber!==void 0).length,f=n.filter(c=>!c.hasSpec).length;ae(n.length,f,i),R=ce(n),R()}const de=ge(r,()=>R());return me(),{card:r,hasData:se,setTest:n=>{X!==n&&(X=n,R())},destroy:()=>de.disconnect()}}
|