@wafertools/wafermap 0.21.1
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 +751 -0
- package/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/packages/canvas-adapter/canvasTheme.d.ts +28 -0
- package/dist/packages/canvas-adapter/canvasTheme.js +1 -0
- package/dist/packages/canvas-adapter/charts/barPanel.d.ts +52 -0
- package/dist/packages/canvas-adapter/charts/barPanel.js +1 -0
- package/dist/packages/canvas-adapter/charts/binCluster.d.ts +17 -0
- package/dist/packages/canvas-adapter/charts/binCluster.js +1 -0
- package/dist/packages/canvas-adapter/charts/boxplot.d.ts +45 -0
- package/dist/packages/canvas-adapter/charts/boxplot.js +1 -0
- package/dist/packages/canvas-adapter/charts/capability.d.ts +41 -0
- package/dist/packages/canvas-adapter/charts/capability.js +1 -0
- package/dist/packages/canvas-adapter/charts/chartShell.d.ts +156 -0
- package/dist/packages/canvas-adapter/charts/chartShell.js +1 -0
- package/dist/packages/canvas-adapter/charts/correlation.d.ts +35 -0
- package/dist/packages/canvas-adapter/charts/correlation.js +1 -0
- package/dist/packages/canvas-adapter/charts/histogram.d.ts +30 -0
- package/dist/packages/canvas-adapter/charts/histogram.js +1 -0
- package/dist/packages/canvas-adapter/charts/index.d.ts +16 -0
- package/dist/packages/canvas-adapter/charts/index.js +1 -0
- package/dist/packages/canvas-adapter/charts/palette.d.ts +26 -0
- package/dist/packages/canvas-adapter/charts/palette.js +1 -0
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +18 -0
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -0
- package/dist/packages/canvas-adapter/charts/scatter.d.ts +37 -0
- package/dist/packages/canvas-adapter/charts/scatter.js +1 -0
- package/dist/packages/canvas-adapter/icons.d.ts +2 -0
- package/dist/packages/canvas-adapter/icons.js +42 -0
- package/dist/packages/canvas-adapter/index.d.ts +12 -0
- package/dist/packages/canvas-adapter/index.js +1 -0
- package/dist/packages/canvas-adapter/insightsTab.d.ts +76 -0
- package/dist/packages/canvas-adapter/insightsTab.js +1 -0
- package/dist/packages/canvas-adapter/metadataBadge.d.ts +26 -0
- package/dist/packages/canvas-adapter/metadataBadge.js +1 -0
- package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +181 -0
- package/dist/packages/canvas-adapter/renderWaferGallery.js +20 -0
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +316 -0
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -0
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +262 -0
- package/dist/packages/canvas-adapter/summaryPanel.js +3 -0
- package/dist/packages/canvas-adapter/toCanvas.d.ts +106 -0
- package/dist/packages/canvas-adapter/toCanvas.js +1 -0
- package/dist/packages/canvas-adapter/toolbar.d.ts +496 -0
- package/dist/packages/canvas-adapter/toolbar.js +9 -0
- package/dist/packages/canvas-adapter/userGuideHtml.d.ts +2 -0
- package/dist/packages/canvas-adapter/userGuideHtml.js +968 -0
- package/dist/packages/canvas-adapter/version.d.ts +3 -0
- package/dist/packages/canvas-adapter/version.js +1 -0
- package/dist/packages/core/aggregates.d.ts +54 -0
- package/dist/packages/core/aggregates.js +1 -0
- package/dist/packages/core/classify.d.ts +24 -0
- package/dist/packages/core/classify.js +1 -0
- package/dist/packages/core/dies.d.ts +92 -0
- package/dist/packages/core/dies.js +1 -0
- package/dist/packages/core/index.d.ts +13 -0
- package/dist/packages/core/index.js +1 -0
- package/dist/packages/core/inference/grid.d.ts +35 -0
- package/dist/packages/core/inference/grid.js +1 -0
- package/dist/packages/core/inference/index.d.ts +7 -0
- package/dist/packages/core/inference/index.js +1 -0
- package/dist/packages/core/inference/pitch.d.ts +26 -0
- package/dist/packages/core/inference/pitch.js +1 -0
- package/dist/packages/core/inference/wafer.d.ts +30 -0
- package/dist/packages/core/inference/wafer.js +1 -0
- package/dist/packages/core/metadata.d.ts +31 -0
- package/dist/packages/core/metadata.js +1 -0
- package/dist/packages/core/probe.d.ts +16 -0
- package/dist/packages/core/probe.js +1 -0
- package/dist/packages/core/reticle.d.ts +68 -0
- package/dist/packages/core/reticle.js +1 -0
- package/dist/packages/core/transforms.d.ts +142 -0
- package/dist/packages/core/transforms.js +1 -0
- package/dist/packages/core/utils.d.ts +60 -0
- package/dist/packages/core/utils.js +1 -0
- package/dist/packages/core/wafer.d.ts +45 -0
- package/dist/packages/core/wafer.js +1 -0
- package/dist/packages/renderer/buildView.d.ts +420 -0
- package/dist/packages/renderer/buildView.js +1 -0
- package/dist/packages/renderer/buildWaferMap.d.ts +543 -0
- package/dist/packages/renderer/buildWaferMap.js +1 -0
- package/dist/packages/renderer/colorMap.d.ts +46 -0
- package/dist/packages/renderer/colorMap.js +1 -0
- package/dist/packages/renderer/colorSchemes.d.ts +40 -0
- package/dist/packages/renderer/colorSchemes.js +1 -0
- package/dist/packages/renderer/fmt.d.ts +30 -0
- package/dist/packages/renderer/fmt.js +1 -0
- package/dist/packages/renderer/index.d.ts +7 -0
- package/dist/packages/renderer/index.js +1 -0
- package/dist/packages/stats/analyzeWaferLot.d.ts +5 -0
- package/dist/packages/stats/analyzeWaferLot.js +1 -0
- package/dist/packages/stats/analyzeWaferMap.d.ts +13 -0
- package/dist/packages/stats/analyzeWaferMap.js +1 -0
- package/dist/packages/stats/binPareto.d.ts +49 -0
- package/dist/packages/stats/binPareto.js +1 -0
- package/dist/packages/stats/boxplot.d.ts +39 -0
- package/dist/packages/stats/boxplot.js +1 -0
- package/dist/packages/stats/capability.d.ts +55 -0
- package/dist/packages/stats/capability.js +1 -0
- package/dist/packages/stats/clusterDetection.d.ts +18 -0
- package/dist/packages/stats/clusterDetection.js +1 -0
- package/dist/packages/stats/correlation.d.ts +55 -0
- package/dist/packages/stats/correlation.js +1 -0
- package/dist/packages/stats/facets.d.ts +74 -0
- package/dist/packages/stats/facets.js +1 -0
- package/dist/packages/stats/filterFindings.d.ts +9 -0
- package/dist/packages/stats/filterFindings.js +1 -0
- package/dist/packages/stats/findingsNarrative.d.ts +3 -0
- package/dist/packages/stats/findingsNarrative.js +1 -0
- package/dist/packages/stats/histogram.d.ts +42 -0
- package/dist/packages/stats/histogram.js +1 -0
- package/dist/packages/stats/index.d.ts +27 -0
- package/dist/packages/stats/index.js +1 -0
- package/dist/packages/stats/math.d.ts +10 -0
- package/dist/packages/stats/math.js +1 -0
- package/dist/packages/stats/patternClassification.d.ts +107 -0
- package/dist/packages/stats/patternClassification.js +1 -0
- package/dist/packages/stats/regions.d.ts +57 -0
- package/dist/packages/stats/regions.js +1 -0
- package/dist/packages/stats/renderFindingsReport.d.ts +7 -0
- package/dist/packages/stats/renderFindingsReport.js +41 -0
- package/dist/packages/stats/renderSummaryReport.d.ts +60 -0
- package/dist/packages/stats/renderSummaryReport.js +68 -0
- package/dist/packages/stats/reportHtml.d.ts +42 -0
- package/dist/packages/stats/reportHtml.js +301 -0
- package/dist/packages/stats/scatter.d.ts +24 -0
- package/dist/packages/stats/scatter.js +1 -0
- package/dist/packages/stats/types.d.ts +269 -0
- package/dist/packages/stats/types.js +1 -0
- package/dist/packages/stats/yield.d.ts +56 -0
- package/dist/packages/stats/yield.js +1 -0
- package/dist/packages/worker/index.d.ts +44 -0
- package/dist/packages/worker/index.js +1 -0
- package/dist/packages/worker/wafermap.worker.d.ts +44 -0
- package/dist/packages/worker/wafermap.worker.js +1 -0
- package/package.json +122 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paul Robins
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# wafermap
|
|
2
|
+
|
|
3
|
+
[](https://github.com/wafertools/wafermap/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@wafertools/wafermap)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
<img src="docs/images/hero-test-values.png" alt="wafermap demo" style="max-width:640px; display:block; margin:8px 0;" />
|
|
10
|
+
|
|
11
|
+
Browser-first wafer map visualization for semiconductor test data.
|
|
12
|
+
|
|
13
|
+
**Zero runtime dependencies · ~40 kB min+gz (core entry).** Pure ES modules with TypeScript types — works in React, Svelte, Vue, plain HTML, or a Web Worker.
|
|
14
|
+
|
|
15
|
+
**[Project Portal: Docs & Interactive Demos →](https://wafertools.github.io/wafermap/)**
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
wafermap renders interactive wafer maps from semiconductor prober output. Hard bins, soft bins, test values, retest runs, edge exclusion, and spec limits are native inputs.
|
|
20
|
+
|
|
21
|
+
- Geometry inference — pass full physical dimensions or raw prober step positions; die pitch, wafer diameter, and coordinate origin are resolved automatically
|
|
22
|
+
- `renderWaferMap` — interactive canvas map with toolbar, zoom/pan, tooltips, die selection, and summary panel
|
|
23
|
+
- `renderWaferGallery` — lot-level card grid with shared controls and click-to-expand
|
|
24
|
+
- 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
|
|
25
|
+
- `analyzeWaferMap` / `analyzeWaferLot` — spatial analysis across rings, quadrants, sectors, and reticle positions; failure cluster detection; lot trend series
|
|
26
|
+
- Pure ES modules, no server, no runtime dependencies — works in React, Svelte, Vue, plain HTML, or a Web Worker
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @wafertools/wafermap
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { buildWaferMap } from '@wafertools/wafermap';
|
|
36
|
+
import { renderWaferMap } from '@wafertools/wafermap/render';
|
|
37
|
+
|
|
38
|
+
const result = buildWaferMap({
|
|
39
|
+
results: rows.map(r => ({ x: +r.x, y: +r.y, hbin: +r.hbin })),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
renderWaferMap(document.getElementById('map'), result);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Note the two import paths. The renderers (`renderWaferMap`, `renderWaferGallery`,
|
|
46
|
+
`toCanvas`) live **only** at `@wafertools/wafermap/render`, so importing them from the
|
|
47
|
+
root package will fail. That keeps the root entry DOM-free — usable in Node for a
|
|
48
|
+
build-and-analyse pipeline, and tree-shakeable when you only need the geometry, data
|
|
49
|
+
and stats layers.
|
|
50
|
+
|
|
51
|
+
## Docs
|
|
52
|
+
|
|
53
|
+
**Building an app with wafermap** (developers):
|
|
54
|
+
|
|
55
|
+
- [Quick start](https://wafertools.github.io/wafermap/quickstart/)
|
|
56
|
+
- [Developer Guide](https://wafertools.github.io/wafermap/guide/)
|
|
57
|
+
- [SvelteKit](https://wafertools.github.io/wafermap/sveltekit/) · [React](https://wafertools.github.io/wafermap/react/) · [Vue 3](https://wafertools.github.io/wafermap/vue/) integration guides
|
|
58
|
+
- [API Reference](https://wafertools.github.io/wafermap/api/)
|
|
59
|
+
- [Architecture](https://wafertools.github.io/wafermap/architecture/) · [Performance](https://wafertools.github.io/wafermap/performance/) · [Troubleshooting](https://wafertools.github.io/wafermap/troubleshooting/)
|
|
60
|
+
- [Live examples](https://wafertools.github.io/wafermap/examples/)
|
|
61
|
+
|
|
62
|
+
**Using an app built with wafermap** (test / device / yield engineers):
|
|
63
|
+
|
|
64
|
+
- [Application User Guide](https://wafertools.github.io/wafermap/user-guide/) — also embedded in apps via the toolbar help button
|
|
65
|
+
- [Glossary](https://wafertools.github.io/wafermap/glossary/)
|
|
66
|
+
|
|
67
|
+
## Built with wafermap
|
|
68
|
+
|
|
69
|
+
- **[tsmap](https://github.com/wafertools/tsmap)** — cross-platform desktop app for loading STDF, ATDF, CSV, and JSON wafer data
|
|
70
|
+
|
|
71
|
+
## Local preview
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install
|
|
75
|
+
npm run dev
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Serves the documentation site locally from `docs/`, including the
|
|
79
|
+
example pages under `docs/examples/`.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export*from"./packages/core/index.js";export*from"./packages/renderer/index.js";export*from"./packages/stats/index.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Chrome colours drawn onto the canvas, resolved from `--wmap-*` per draw. */
|
|
2
|
+
export interface CanvasTheme {
|
|
3
|
+
/** Canvas background fill. */
|
|
4
|
+
background: string;
|
|
5
|
+
/** Raised surface fill (e.g. gallery-export header strip). */
|
|
6
|
+
surface: string;
|
|
7
|
+
/** Primary on-canvas text (axis labels, legend labels, die-count). */
|
|
8
|
+
text: string;
|
|
9
|
+
/** Muted on-canvas text (overflow "+N", secondary counts). */
|
|
10
|
+
textMuted: string;
|
|
11
|
+
/** Axis tick lines / faint separators. */
|
|
12
|
+
axisLine: string;
|
|
13
|
+
/** Active-selection highlight (legend row, active bin). */
|
|
14
|
+
accent: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the canvas chrome palette from `--wmap-*` custom properties on `el`
|
|
18
|
+
* (the render container, where a host sets its theme). Call ONCE per draw.
|
|
19
|
+
* Each token falls back to its light default, so a host that sets no variables
|
|
20
|
+
* gets the previous appearance exactly.
|
|
21
|
+
*
|
|
22
|
+
* `background` maps to `--wmap-canvas-bg` (a canvas can be a different shade
|
|
23
|
+
* from the DOM surface — often a touch darker/lighter than the panel), falling
|
|
24
|
+
* back to `--wmap-surface` and then the light default, so hosts that only set
|
|
25
|
+
* the general surface token still get a sensible canvas.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveCanvasTheme(el: HTMLElement | null): CanvasTheme;
|
|
28
|
+
//# sourceMappingURL=canvasTheme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e={background:"#f5f5f5",surface:"#fff",text:"#333",textMuted:"#999",axisLine:"#bbb",accent:"#1a66cc"};export function resolveCanvasTheme(c){if(!c||typeof getComputedStyle!="function")return{...e};const a=getComputedStyle(c),t=(n,r)=>a.getPropertyValue(`--wmap-${n}`).trim()||r;return{background:t("canvas-bg",t("surface",e.background)),surface:t("surface",e.surface),text:t("text",e.text),textMuted:t("text-muted",e.textMuted),axisLine:t("border",e.axisLine),accent:t("icon-active",e.accent)}}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ChartDatum } from '../../stats/yield.js';
|
|
2
|
+
import { type SaveImageHandler } from './chartShell.js';
|
|
3
|
+
export interface ChartPanel {
|
|
4
|
+
title: string;
|
|
5
|
+
data: ChartDatum[];
|
|
6
|
+
barColor?: (datum: ChartDatum, index: number) => string;
|
|
7
|
+
valueLabel?: (datum: ChartDatum) => string;
|
|
8
|
+
/**
|
|
9
|
+
* Optional self-contained segmented control (yield sort, hard/soft bins).
|
|
10
|
+
* When present the panel renders a radio group and, on change, recomputes
|
|
11
|
+
* its own data and redraws in place.
|
|
12
|
+
*/
|
|
13
|
+
selfControl?: {
|
|
14
|
+
current: string;
|
|
15
|
+
options: Array<[value: string, label: string]>;
|
|
16
|
+
onChange: (value: string) => {
|
|
17
|
+
data: ChartDatum[];
|
|
18
|
+
title?: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* In-place drill-down for a grouped row (`itemCount > 1`). When present,
|
|
23
|
+
* clicking such a row calls `onOpenGroup`, which returns the detail
|
|
24
|
+
* data/title to redraw in place (no modal, no grid rebuild) and shows a
|
|
25
|
+
* Back button; clicking it calls `onBack`, which returns the overview
|
|
26
|
+
* data/title.
|
|
27
|
+
*/
|
|
28
|
+
drill?: {
|
|
29
|
+
onOpenGroup: (datum: ChartDatum) => {
|
|
30
|
+
data: ChartDatum[];
|
|
31
|
+
title: string;
|
|
32
|
+
};
|
|
33
|
+
onBack: () => {
|
|
34
|
+
data: ChartDatum[];
|
|
35
|
+
title: string;
|
|
36
|
+
};
|
|
37
|
+
groupLabelText: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Clicking a leaf row (`itemCount === 1` — a single wafer, whether reached
|
|
41
|
+
* directly or by drilling into a group first) calls this to open that
|
|
42
|
+
* wafer's map. A group row (`itemCount > 1`, drill not yet active) drills
|
|
43
|
+
* instead, via `drill.onOpenGroup` — this is never called for those.
|
|
44
|
+
*/
|
|
45
|
+
onOpen?: (datum: ChartDatum) => void;
|
|
46
|
+
}
|
|
47
|
+
export interface BarPanelHandle {
|
|
48
|
+
card: HTMLElement;
|
|
49
|
+
destroy: () => void;
|
|
50
|
+
}
|
|
51
|
+
export declare function renderBarPanel(panel: ChartPanel, onSaveImage?: SaveImageHandler): BarPanelHandle;
|
|
52
|
+
//# sourceMappingURL=barPanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CLR as F}from"../toolbar.js";import{cardShell as U,formatValue as N,observeResize as q,makeTooltip as J,positionChartTooltip as K,makeBackButton as Q,makeSegmented as Z,growCardToFitContent as tt,resolveChartCanvasColors as et,PADDING as m,VALUE_WIDTH as D}from"./chartShell.js";const a=24,p=5,G=110,nt=12;export function renderBarPanel(r,I){const{barColor:O,valueLabel:A}=r;let d=r.title,i=r.data;const{card:h,heading:k,controlsRow:E,body:R}=U(d,I);if(r.selfControl){const t=r.selfControl;E.appendChild(Z(t.options,t.current,n=>X(n)))}const{drill:s}=r;let b=!1,v=null;const $=document.createElement("div");Object.assign($.style,{color:F.label,fontSize:"11px",marginBottom:"6px"}),h.insertBefore($,R);function B(){const t=[];r.onOpen&&t.push("click to open this wafer"),s&&!b&&t.push(`click a ${s.groupLabelText} to see it by wafer`);const n=t.join(", or ");$.textContent=n?`${n[0].toUpperCase()}${n.slice(1)}.`:""}B();const C=document.createElement("div"),L=()=>m*2+Math.min(i.length,nt)*(a+p);Object.assign(C.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${L()}px`,scrollbarGutter:"stable"}),R.appendChild(C);const o=document.createElement("canvas");o.style.display="block",o.style.cursor="default",C.appendChild(o);const y=J(h);let c=-1;const w=window.devicePixelRatio||1;let M=Math.max(1,...i.map(t=>t.value));const S=t=>A?A(t):`${N(t.value)} (${t.percent.toFixed(1)}%)`;function _(t){const n=m+t*(a+p),e=m+G,l=o.clientWidth-e-D-m;return{y:n,barX:e,barMaxWidth:Math.max(10,l)}}function u(){C.style.maxHeight=`${L()}px`,tt(h,R,L());const t=C.clientWidth,n=m*2+i.length*(a+p);o.width=Math.max(1,Math.floor(t*w)),o.height=Math.max(1,Math.floor(n*w)),o.style.width=`${t}px`,o.style.height=`${n}px`;const e=o.getContext("2d");e.setTransform(w,0,0,w,0,0),e.clearRect(0,0,t,n),e.font="11px system-ui, sans-serif",e.textBaseline="middle";const l=et(h);i.forEach((f,g)=>{const{y:x,barX:T,barMaxWidth:H}=_(g);g===c&&(e.fillStyle=l.bgHover,e.fillRect(0,x-p/2,t,a+p)),e.fillStyle=l.text,e.textAlign="right";const j=f.label.length>16?`${f.label.slice(0,15)}\u2026`:f.label;e.fillText(j,m+G-8,x+a/2),e.fillStyle=l.track,e.fillRect(T,x,H,a);const z=Math.max(1,f.value/M*H);e.fillStyle=O?O(f,g):l.text,e.fillRect(T,x,z,a),e.fillStyle=l.text,e.textAlign="right",e.fillText(S(f),T+H+D,x+a/2)})}function X(t){if(!r.selfControl)return;r.selfControl.current=t;const n=r.selfControl.onChange(t);i=n.data,M=Math.max(1,...i.map(e=>e.value)),c=-1,n.title&&(d=n.title,k.textContent=d),u()}function P(t){if(!s)return;const n=s.onOpenGroup(t);i=n.data,d=n.title,k.textContent=d,M=Math.max(1,...i.map(e=>e.value)),c=-1,b||(b=!0,v=Q(()=>V()),E.appendChild(v)),B(),u()}function V(){if(!s)return;const t=s.onBack();i=t.data,d=t.title,k.textContent=d,M=Math.max(1,...i.map(n=>n.value)),c=-1,b=!1,v?.remove(),v=null,B(),u()}function W(t){const n=Math.floor((t-m+p/2)/(a+p));return n>=0&&n<i.length?n:-1}o.addEventListener("mousemove",t=>{const n=o.getBoundingClientRect(),e=W(t.clientY-n.top),l=e>=0&&s&&!b&&i[e].itemCount>1,f=e>=0&&(l||!!r.onOpen&&e>=0&&!l);if(e!==c&&(c=e,o.style.cursor=f?"pointer":"default",u()),e>=0){const g=i[e],x=l?`<br><em>click to see this ${s.groupLabelText} by wafer</em>`:r.onOpen?"<br><em>click to open this wafer</em>":"";y.innerHTML=`<strong>${g.label}</strong><br>${S(g)}${x}`,y.style.display="block",K(y,h,t.clientX,t.clientY)}else y.style.display="none"}),o.addEventListener("mouseleave",()=>{c!==-1&&(c=-1,u()),y.style.display="none"}),o.addEventListener("click",t=>{const n=o.getBoundingClientRect(),e=W(t.clientY-n.top);if(e===-1)return;const l=i[e];if(s&&!b&&l.itemCount>1){P(l);return}r.onOpen?.(l)});const Y=q(h,()=>u());return u(),{card:h,destroy:()=>Y.disconnect()}}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type BinItem } from '../../stats/binPareto.js';
|
|
2
|
+
import { type SaveImageHandler } from './chartShell.js';
|
|
3
|
+
export interface BinClusterPanelOptions {
|
|
4
|
+
title?: string;
|
|
5
|
+
groups: {
|
|
6
|
+
key: string;
|
|
7
|
+
items: BinItem[];
|
|
8
|
+
}[];
|
|
9
|
+
colorScheme?: string;
|
|
10
|
+
onSaveImage?: SaveImageHandler;
|
|
11
|
+
}
|
|
12
|
+
export interface BinClusterPanelHandle {
|
|
13
|
+
card: HTMLElement;
|
|
14
|
+
destroy: () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function renderBinClusterPanel(options: BinClusterPanelOptions): BinClusterPanelHandle;
|
|
17
|
+
//# sourceMappingURL=binCluster.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{buildBinClusterData as Y}from"../../stats/binPareto.js";import{categorical as q}from"./palette.js";import{CLR as G}from"../toolbar.js";import{cardShell as J,observeResize as K,makeTooltip as Q,positionChartTooltip as Z,makeSegmented as tt,renderEmptyState as et,growCardToFitContent as nt,resolveChartCanvasColors as ot,PADDING as a,VALUE_WIDTH as P}from"./chartShell.js";const X=90,lt=8,A=1,f=14,it=8;export function renderBinClusterPanel(L){const{groups:j,onSaveImage:D}=L;let x="hbin",M=L.title??"Hard bin pareto";const{card:c,heading:U,body:p,controlsRow:z}=J(M,D);z.appendChild(tt([["hbin","Hard bins"],["sbin","Soft bins"]],x,h=>{x=h,M=`${x==="hbin"?"Hard":"Soft"} bin pareto`,U.textContent=M,I()}));const w=document.createElement("div");w.textContent="One cluster per bin \xB7 a sub-bar per group",Object.assign(w.style,{color:G.label,fontSize:"11px",marginBottom:"6px"}),c.insertBefore(w,p);const b=Q(c);let y=null;function I(){y?.disconnect(),y=null,p.innerHTML="";const h=Y(j,x);if(h.bins.length===0||h.groups.length===0){et(p,"No bin data available for the current grouping.");return}const u=h.groups,g=h.bins,_=q,N=Math.max(1,...g.flatMap(n=>n.counts)),B=u.length*f+(u.length-1)*A,m=B+lt,C=document.createElement("div");Object.assign(C.style,{display:"flex",flexWrap:"wrap",gap:"4px 12px",marginBottom:"4px"}),u.forEach((n,l)=>{const e=document.createElement("span");Object.assign(e.style,{display:"inline-flex",alignItems:"center",gap:"5px",fontSize:"11px",color:G.text});const t=document.createElement("span");Object.assign(t.style,{width:"10px",height:"10px",borderRadius:"2px",background:_(l)});const o=document.createElement("span");o.textContent=n,e.append(t,o),C.appendChild(e)}),p.appendChild(C);const v=document.createElement("div"),$=a*2+Math.min(g.length,it)*m;Object.assign(v.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${$}px`,scrollbarGutter:"stable"}),nt(c,p,C.offsetHeight+$),p.appendChild(v);const i=document.createElement("canvas");i.style.display="block",i.style.cursor="default",v.appendChild(i);const E=window.devicePixelRatio||1;let r=null;function W(){const n=a+X,l=Math.max(10,i.clientWidth-n-P-a);return{barX:n,barMaxWidth:l}}function F(n,l){const e=Math.floor((l-a)/m);if(e<0||e>=g.length)return null;const t=l-a-e*m,o=Math.floor(t/(f+A));if(o<0||o>=u.length)return null;const{barX:s,barMaxWidth:d}=W();return n<s||n>s+d?null:{bin:e,group:o}}function S(){const n=ot(c),l=v.clientWidth,e=a*2+g.length*m;i.width=Math.max(1,Math.floor(l*E)),i.height=Math.max(1,Math.floor(e*E)),i.style.width=`${l}px`,i.style.height=`${e}px`;const t=i.getContext("2d");t.setTransform(E,0,0,E,0,0),t.clearRect(0,0,l,e),t.font="11px system-ui, sans-serif",t.textBaseline="middle";const{barX:o,barMaxWidth:s}=W();g.forEach((d,O)=>{const T=a+O*m;t.fillStyle=n.text,t.textAlign="right",t.fillText(d.label,a+X-8,T+B/2),u.forEach((st,H)=>{const R=T+H*(f+A),k=d.counts[H];r&&r.bin===O&&r.group===H&&(t.fillStyle=n.bgHover,t.fillRect(0,R-1,l,f+2)),t.fillStyle=n.track,t.fillRect(o,R,s,f);const V=Math.max(k>0?1:0,k/N*s);t.fillStyle=_(H),t.fillRect(o,R,V,f)}),t.fillStyle=n.text,t.textAlign="right",t.fillText(`${d.total}`,o+s+P,T+B/2)})}i.addEventListener("mousemove",n=>{const l=i.getBoundingClientRect(),e=F(n.clientX-l.left,n.clientY-l.top),t=e?.bin!==r?.bin||e?.group!==r?.group;if(r=e,t&&S(),e){const o=g[e.bin],s=o.counts[e.group],d=o.total>0?s/o.total*100:0;b.innerHTML=`<strong>${o.label}</strong> \xB7 ${u[e.group]}<br>${s} dies (${d.toFixed(1)}% of bin)`,b.style.display="block",Z(b,c,n.clientX,n.clientY)}else b.style.display="none"}),i.addEventListener("mouseleave",()=>{r&&(r=null,S()),b.style.display="none"}),y=K(c,()=>S()),S()}return I(),{card:c,destroy:()=>y?.disconnect()}}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type BoxplotItem } from '../../stats/boxplot.js';
|
|
2
|
+
import type { TestDef } from '../../renderer/buildWaferMap.js';
|
|
3
|
+
import { type SaveImageHandler } from './chartShell.js';
|
|
4
|
+
/** A `BoxplotItem` carrying enough identity for the panel to open its wafer
|
|
5
|
+
* on a leaf-row click — the tab's own item shape already has this. */
|
|
6
|
+
export type BoxplotPanelItem = BoxplotItem & {
|
|
7
|
+
waferIndex?: number;
|
|
8
|
+
};
|
|
9
|
+
export interface BoxplotPanelOptions {
|
|
10
|
+
title?: string;
|
|
11
|
+
items: BoxplotPanelItem[];
|
|
12
|
+
testDefs: TestDef[];
|
|
13
|
+
selectedTestNumber?: number;
|
|
14
|
+
colorScheme?: string;
|
|
15
|
+
onSaveImage?: SaveImageHandler;
|
|
16
|
+
/**
|
|
17
|
+
* When the Analysis tab's "Group by" is active, this panel shows one
|
|
18
|
+
* pooled row per group by default (all of that group's items' dies
|
|
19
|
+
* combined into one five-number summary); clicking a group's row drills
|
|
20
|
+
* in place into that group's own per-item rows, with a Back button.
|
|
21
|
+
* `items` above is ignored when `groups` is provided. Absent ⇒ today's
|
|
22
|
+
* plain ungrouped per-item rows, no drill.
|
|
23
|
+
*/
|
|
24
|
+
groups?: {
|
|
25
|
+
key: string;
|
|
26
|
+
items: BoxplotPanelItem[];
|
|
27
|
+
}[];
|
|
28
|
+
/** Human label of the active facet (e.g. "Split"), used in the overview
|
|
29
|
+
* hint text ("click a <label>'s box to see it by wafer"). */
|
|
30
|
+
groupLabelText?: string;
|
|
31
|
+
/** Clicking a leaf row (a real per-item row — ungrouped, or drilled into
|
|
32
|
+
* a group) calls this with that item's `waferIndex` and the boxplot's
|
|
33
|
+
* currently selected `testNumber`, so the opened wafer can land on the
|
|
34
|
+
* same test in value mode instead of defaulting to hard-bin mode.
|
|
35
|
+
* Never called for a pooled group-overview row (that drills instead). */
|
|
36
|
+
onOpen?: (waferIndex: number, testNumber: number) => void;
|
|
37
|
+
}
|
|
38
|
+
export interface BoxplotPanelHandle {
|
|
39
|
+
card: HTMLElement;
|
|
40
|
+
/** Cross-panel link (e.g. from the capability panel): switch to `testNumber` in place. */
|
|
41
|
+
setTest: (testNumber: number) => void;
|
|
42
|
+
destroy: () => void;
|
|
43
|
+
}
|
|
44
|
+
export declare function renderBoxplotPanel(options: BoxplotPanelOptions): BoxplotPanelHandle;
|
|
45
|
+
//# sourceMappingURL=boxplot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{buildTestBoxplotData as It}from"../../stats/boxplot.js";import{CLR as Ht}from"../toolbar.js";import{fmt as gt}from"../../renderer/fmt.js";import{QUANTITY as bt}from"./palette.js";import{cardShell as Rt,observeResize as Wt,makeTooltip as Et,positionChartTooltip as Ot,makeBackButton as _t,makeTestSelect as Dt,makeToggle as yt,renderEmptyState as Tt,growCardToFitContent as Pt,resolveChartCanvasColors as Gt,makeAxisFormat as Xt,PADDING as y,VALUE_WIDTH as Nt}from"./chartShell.js";const T=24,M=5,Mt=110,Yt=12,vt=20;export function renderBoxplotPanel(lt){const{title:N="Test value distribution",items:kt,testDefs:it,onSaveImage:wt,groups:m,groupLabelText:Y="group",onOpen:W}=lt,{card:x,heading:St,body:f,controlsRow:E}=Rt(N,wt);x.style.flex="0 0 auto",f.style.flex="0 0 auto",x.style.alignSelf="start";const O=it.filter(l=>l.testNumber!==void 0);let h=lt.selectedTestNumber??O[0]?.testNumber??null,q=!1,_=!1,d=null,L=null,Q=null,st=null;function Lt(){const l=!m||m.length===0?"":d??"\0overview";if(Q&&st===l)return Q;const n=!m||m.length===0?kt:d!==null?m.find(u=>u.key===d)?.items??[]:m.map(u=>({label:u.key,dies:u.items.flatMap(C=>C.dies??[])}));return Q=n,st=l,n}function U(){d!==null&&!L?(L=_t(()=>{d=null,U(),v()}),E.appendChild(L)):d===null&&L&&(L.remove(),L=null),St.textContent=d!==null?`${N} \u2014 ${Y}: ${d}`:N}const at=Dt(O,h,l=>{h=l,v()},{maxWidth:"240px",emptyText:"No parametric tests"});E.appendChild(at),E.appendChild(yt("Log scale",q,l=>{q=l,v()})),E.appendChild(yt("Axis includes limits",_,l=>{_=l,v()}));const z=document.createElement("div");Object.assign(z.style,{color:Ht.label,fontSize:"11px",marginBottom:"6px"}),x.insertBefore(z,f);function F(){const l=!!m&&m.length>0&&d===null,n=[];l?n.push(`click a ${Y}'s box to see it by wafer`):W&&n.push("click a box to open that wafer");const u=n.length?`${n[0][0].toUpperCase()}${n[0].slice(1)} \xB7 `:"";z.textContent=`${u}box = Q1\u2013Q3, line = median, whiskers = min/max`}F(),U();const $=Et(x);let V=null;function v(){if(F(),f.innerHTML="",O.length===0||h===null){Tt(f,"No parametric test data available for box plots.");return}const l=Lt(),n=It(l,h),u=it.find(e=>e.testNumber===h),C=u?.unit,j=u?.limitLow,K=u?.limitHigh;if(n.every(e=>e.count===0)){Tt(f,"No parametric test data available for box plots.");return}const r=document.createElement("canvas");r.style.display="block",r.style.cursor="default",f.appendChild(r);const rt=y*2+Math.min(n.length,Yt)*(T+M)+vt;f.style.maxHeight=`${rt}px`,f.style.overflowY="auto",f.style.scrollbarGutter="stable";let A=-1;const D=window.devicePixelRatio||1,ct=n.filter(e=>e.count>0),ft=Math.min(...ct.map(e=>e.min)),dt=Math.max(...ct.map(e=>e.max)),B=_&&j!==void 0?Math.min(ft,j):ft,J=_&&K!==void 0?Math.max(dt,K):dt,mt=J-B||1,P=q&&B>0,Z=P?Math.log10(B):0,ut=(P?Math.log10(J):0)-Z||1,tt=Xt(Math.max(Math.abs(B),Math.abs(J)),C);function $t(){const e=y+Mt,o=r.clientWidth-e-Nt-y;return{plotX:e,plotMaxWidth:Math.max(10,o)}}function k(e,o,i){return P?o+(Math.log10(e)-Z)/ut*i:o+(e-B)/mt*i}function Ct(){const e=[];for(let o=0;o<=4;o++)e.push(P?Math.pow(10,Z+ut*o/4):B+mt*o/4);return e}function G(){const e=Gt(x),o=f.clientWidth,i=y*2+n.length*(T+M)+vt;r.width=Math.max(1,Math.floor(o*D)),r.height=Math.max(1,Math.floor(i*D)),r.style.width=`${o}px`,r.style.height=`${i}px`;const t=r.getContext("2d");t.setTransform(D,0,0,D,0,0),t.clearRect(0,0,o,i),t.font="11px system-ui, sans-serif",t.textBaseline="middle";const{plotX:s,plotMaxWidth:c}=$t();n.forEach((a,g)=>{const b=y+g*(T+M),w=b+T/2;g===A&&(t.fillStyle=e.bgHover,t.fillRect(0,b-M/2,o,T+M)),t.fillStyle=e.text,t.textAlign="right";const At=a.label.length>16?`${a.label.slice(0,15)}\u2026`:a.label;if(t.fillText(At,y+Mt-8,w),a.count===0){t.fillStyle=e.textMuted,t.textAlign="left",t.fillText("no data",s,w);return}const et=k(a.min,s,c),H=k(a.q1,s,c),pt=k(a.median,s,c),nt=k(a.q3,s,c),ot=k(a.max,s,c),S=b+3,R=b+T-3;t.strokeStyle=e.textMuted,t.lineWidth=1,t.beginPath(),t.moveTo(et,w),t.lineTo(H,w),t.moveTo(nt,w),t.lineTo(ot,w),t.moveTo(et,S),t.lineTo(et,R),t.moveTo(ot,S),t.lineTo(ot,R),t.stroke(),t.fillStyle=bt,t.globalAlpha=.35,t.fillRect(H,S,Math.max(1,nt-H),R-S),t.globalAlpha=1,t.strokeStyle=bt,t.strokeRect(H,S,Math.max(1,nt-H),R-S),t.strokeStyle=e.text,t.lineWidth=2,t.beginPath(),t.moveTo(pt,S),t.lineTo(pt,R),t.stroke(),t.lineWidth=1,t.fillStyle=e.textMuted,t.textAlign="left",t.fillText(`med ${gt(a.median,C,"engineering")}`,s+c+10,w)});const p=y+n.length*(T+M);t.font="10px system-ui, sans-serif";for(const[a,g]of[[j,"LSL"],[K,"USL"]]){if(a===void 0)continue;const b=k(a,s,c);t.strokeStyle=e.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(b,0),t.lineTo(b,p),t.stroke(),t.setLineDash([]),t.fillStyle=e.warnBorder,t.textAlign="center",t.textBaseline="bottom",t.fillText(g,b,p-1)}t.font="11px system-ui, sans-serif",t.strokeStyle=e.border,t.lineWidth=1,t.beginPath(),t.moveTo(s,p),t.lineTo(s+c,p),t.stroke(),t.fillStyle=e.textMuted,t.textAlign="center",t.textBaseline="top";for(const a of Ct()){const g=k(a,s,c);t.beginPath(),t.moveTo(g,p),t.lineTo(g,p+4),t.stroke(),t.fillText(tt.tick(a),g,p+6)}tt.unitLabel&&(t.textAlign="left",t.font="10px system-ui, sans-serif",t.fillText(`(${tt.unitLabel})`,s+c+22,p+6),t.font="11px system-ui, sans-serif"),t.textBaseline="middle"}function xt(e){const o=Math.floor((e-y+M/2)/(T+M));return o>=0&&o<n.length?o:-1}function I(e){return gt(e,C,"engineering")}const X=!!m&&m.length>0&&d===null,ht=e=>!X&&!!W&&l[e]?.waferIndex!==void 0;r.addEventListener("mousemove",e=>{const o=r.getBoundingClientRect(),i=xt(e.clientY-o.top),t=i>=0&&n[i].count>0&&(X||ht(i));if(i!==A&&(A=i,r.style.cursor=t?"pointer":"default",G()),i>=0&&n[i].count>0){const s=n[i],c=X?`<br><em>click to see this ${Y} by wafer</em>`:ht(i)?"<br><em>click to open this wafer</em>":"";$.innerHTML=`<strong>${s.label}</strong> (${s.count} dies)<br>max ${I(s.max)}<br>q3 ${I(s.q3)}<br>median ${I(s.median)}<br>q1 ${I(s.q1)}<br>min ${I(s.min)}${c}`,$.style.display="block",Ot($,x,e.clientX,e.clientY)}else $.style.display="none"}),r.addEventListener("mouseleave",()=>{A!==-1&&(A=-1,G()),$.style.display="none"}),r.addEventListener("click",e=>{const o=r.getBoundingClientRect(),i=xt(e.clientY-o.top);if(i===-1||n[i].count===0)return;if(X){d=n[i].label,U(),F(),v();return}const t=l[i]?.waferIndex;W&&t!==void 0&&h!==null&&W(t,h)}),V?.disconnect(),V=Wt(x,()=>G()),G(),Pt(x,f,rt)}v();function Bt(l){!O.some(n=>n.testNumber===l)||l===h||(h=l,at.value=String(l),v())}return{card:x,setTest:Bt,destroy:()=>V?.disconnect()}}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type CapabilityItem } from '../../stats/capability.js';
|
|
2
|
+
import type { TestDef } from '../../renderer/buildWaferMap.js';
|
|
3
|
+
import { type SaveImageHandler } from './chartShell.js';
|
|
4
|
+
export interface CapabilityPanelOptions {
|
|
5
|
+
title?: string;
|
|
6
|
+
items: CapabilityItem[];
|
|
7
|
+
testDefs: TestDef[];
|
|
8
|
+
colorScheme?: string;
|
|
9
|
+
onSaveImage?: SaveImageHandler;
|
|
10
|
+
/** 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
|
+
onSelectTest?: (testNumber: number) => void;
|
|
12
|
+
/**
|
|
13
|
+
* When the Analysis tab's "Group by" is active, this panel gets its own
|
|
14
|
+
* "Group: <value> ▾" restrict-to-one-group dropdown (matching tsmap's
|
|
15
|
+
* `charts/capability.ts` exactly — capability shows exactly one group's
|
|
16
|
+
* data at a time, never all groups at once or pooled). `items` above is
|
|
17
|
+
* ignored when `groups` is provided; the active group's own item list is
|
|
18
|
+
* used instead. Absent ⇒ today's plain ungrouped behavior.
|
|
19
|
+
*/
|
|
20
|
+
groups?: {
|
|
21
|
+
key: string;
|
|
22
|
+
items: CapabilityItem[];
|
|
23
|
+
}[];
|
|
24
|
+
}
|
|
25
|
+
export interface CapabilityPanelHandle {
|
|
26
|
+
card: HTMLElement;
|
|
27
|
+
/**
|
|
28
|
+
* Whether any test in `testDefs` is parametric at all (has a `testNumber`)
|
|
29
|
+
* — a chart with zero parametric tests has nothing to draw regardless of
|
|
30
|
+
* spec-limit coverage (see `buildCapabilityData`'s fallback tier: tests
|
|
31
|
+
* without spec limits still render, so lacking limits is no longer an
|
|
32
|
+
* empty-chart condition on its own). This is a property of `testDefs`
|
|
33
|
+
* alone, not the selected group. The Analysis tab uses this to avoid
|
|
34
|
+
* forcing a large fixed height on a card that has nothing to draw.
|
|
35
|
+
*/
|
|
36
|
+
hasData: boolean;
|
|
37
|
+
/** Disconnect this panel's own ResizeObserver. Call when removing the card from the DOM. */
|
|
38
|
+
destroy: () => void;
|
|
39
|
+
}
|
|
40
|
+
export declare function renderCapabilityPanel(options: CapabilityPanelOptions): CapabilityPanelHandle;
|
|
41
|
+
//# sourceMappingURL=capability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{buildCapabilityData as ct}from"../../stats/capability.js";import{capabilityColor as dt}from"./palette.js";import{CLR as V}from"../toolbar.js";import{cardShell as pt,chartFillHeight as mt,applyCanvasFlow as ht,observeResize as ut,makeTooltip as ft,positionChartTooltip as yt,makeLabeledSelect as bt,renderEmptyState as xt,resolveChartCanvasColors as vt}from"./chartShell.js";import{fmt as gt}from"../../renderer/fmt.js";const Mt=30,kt=160,Y=90,J=12;function E(w){return w===null?"\u2014":w.toFixed(2)}export function renderCapabilityPanel(w){const{title:K="Process capability",items:Z,testDefs:H,onSaveImage:tt,onSelectTest:R,groups:p}=w,{card:y,body:m,controlsRow:et}=pt(K,tt),nt=H.some(e=>e.testNumber!==void 0);m.style.overflowX="auto";let B=p&&p.length>0?p[0].key:void 0;p&&p.length>0&&et.appendChild(bt("Group:",p.map(e=>({value:e.key,label:e.key})),B??"",e=>{B=e,O()}));function ot(){return!p||p.length===0?Z:p.find(e=>e.key===B)?.items??[]}const A=document.createElement("div");Object.assign(A.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),y.insertBefore(A,m);const M=window.devicePixelRatio||1,k=ft(y);let D=()=>{};function lt(e,n,h){A.innerHTML="";const c=document.createElement("span");Object.assign(c.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:V.value,fontSize:"12px",fontWeight:"500"});const S=h-e,C=n>0?` \xB7 ${n} without spec limits`:"",T=document.createElement("span");T.textContent=S>0?`${e} of ${h} tests shown${C} \xB7 ${S} excluded (no recorded values)`:`${e} test${e!==1?"s":""} shown${C}`,c.appendChild(T);const L=document.createElement("span");L.textContent="\u24D8",L.title="Spec-limited tests are normalized so LSL=0 and USL=1, sorted worst Ppk first. Tests without both spec limits (muted, dashed) are normalized to their own observed range, sorted most-variable first. Box colour: green Ppk \u2265 1.33 (capable), orange Ppk \u2265 1.0 (marginal), red Ppk < 1.0 (poor).",Object.assign(L.style,{color:V.label,cursor:"help",fontSize:"12px"}),c.appendChild(L),A.appendChild(c)}function it(e){if(m.innerHTML="",e.length===0)return xt(m,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const n=document.createElement("canvas");n.style.display="block",n.style.cursor="default",m.appendChild(n);const h=e.length,c=Math.min(0,...e.map(a=>a.min)),S=Math.max(1,...e.map(a=>a.max)),C=(S-c)*.05||.1,T=c-C,st=S+C-T||1;function X(a){return Math.max(Mt,Math.min(kt,Math.floor(a/h)))}function b(a,o,l){return o+(1-(a-T)/st)*l}let $=-1;function N(){ht(n);const a=m.clientWidth,o=X(a),l=o*h,i=J,x=l,u=mt(y,m,n,J+200+Y),_=Math.max(i+60,u-Y),f=_-i;n.width=Math.max(1,Math.floor(x*M)),n.height=Math.max(1,Math.floor(u*M)),n.style.width=`${x}px`,n.style.height=`${u}px`;const t=n.getContext("2d");t.setTransform(M,0,0,M,0,0),t.clearRect(0,0,x,u);const v=vt(y);e.forEach((r,j)=>{const g=j*o,s=g+o/2,d=Math.max(4,o*.55);if(j===$&&(t.fillStyle=v.bgHover,t.fillRect(g,i,o,_-i)),r.hasSpec){const q=b(0,i,f),Q=b(1,i,f);t.strokeStyle=v.warnBorder,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(g,q),t.lineTo(g+o,q),t.moveTo(g,Q),t.lineTo(g+o,Q),t.stroke(),t.setLineDash([])}const G=r.hasSpec?dt(r.ppk):v.textMuted,z=b(r.min,i,f),P=b(r.q1,i,f),U=b(r.median,i,f),W=b(r.q3,i,f),I=b(r.max,i,f);t.strokeStyle=v.textMuted,t.lineWidth=1,t.beginPath(),t.moveTo(s,z),t.lineTo(s,P),t.moveTo(s,W),t.lineTo(s,I),t.moveTo(s-d/4,z),t.lineTo(s+d/4,z),t.moveTo(s-d/4,I),t.lineTo(s+d/4,I),t.stroke(),t.fillStyle=G,t.globalAlpha=.5,t.fillRect(s-d/2,Math.min(P,W),d,Math.max(1,Math.abs(W-P))),t.globalAlpha=1,t.strokeStyle=G,r.hasSpec||t.setLineDash([4,2]),t.strokeRect(s-d/2,Math.min(P,W),d,Math.max(1,Math.abs(W-P))),t.setLineDash([]),t.strokeStyle=v.text,t.lineWidth=2,t.beginPath(),t.moveTo(s-d/2,U),t.lineTo(s+d/2,U),t.stroke(),t.lineWidth=1;const rt=r.label.length>12?`${r.label.slice(0,11)}\u2026`:r.label;t.save(),t.translate(s,_+6),t.rotate(-Math.PI/4),t.textAlign="right",t.textBaseline="middle",t.font="10px system-ui, sans-serif",t.fillStyle=v.textMuted,t.fillText(rt,0,0),t.restore()}),t.strokeStyle=v.border,t.lineWidth=1,t.strokeRect(0,i,l,f)}function F(a){const o=n.getBoundingClientRect(),l=m.clientWidth,i=X(l),x=(a.clientX-o.left)*(n.width/M/o.width),u=Math.floor(x/i);return u>=0&&u<h?u:-1}return n.addEventListener("mousemove",a=>{const o=F(a);if(o!==$&&($=o,N()),o===-1){k.style.display="none";return}const l=e[o],i=x=>gt(x,l.unit);k.innerHTML=`<strong>${l.label}</strong> (n=${l.n})<br>`+(l.hasSpec?`LSL ${i(l.lsl)} \xB7 USL ${i(l.usl)}<br>mean ${i(l.mean)}<br>Cp ${E(l.cp)} \xB7 Cpk ${E(l.cpk)}<br>Pp ${E(l.pp)} \xB7 Ppk ${E(l.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${i(l.mean)} \xB7 stddev ${i(l.stdOverall)}`)+(R?"<br><em>click to view in boxplot</em>":""),k.style.display="block",yt(k,y,a.clientX,a.clientY),n.style.cursor=R?"pointer":"default"}),n.addEventListener("mouseleave",()=>{$!==-1&&($=-1,N()),k.style.display="none"}),n.addEventListener("click",a=>{if(!R)return;const o=F(a);o!==-1&&R(e[o].testNumber)}),N}function O(){const e=ct(ot(),H),n=H.filter(c=>c.testNumber!==void 0).length,h=e.filter(c=>!c.hasSpec).length;lt(e.length,h,n),D=it(e),D()}const at=ut(y,()=>D());return O(),{card:y,hasData:nt,destroy:()=>at.disconnect()}}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { type SaveImageHandler } from '../toolbar.js';
|
|
2
|
+
export type { SaveImageHandler };
|
|
3
|
+
/**
|
|
4
|
+
* `triggerBtn`, when given, is hidden for the modal's duration and shown
|
|
5
|
+
* again on close — a card already sitting inside its own expand modal must
|
|
6
|
+
* not offer a second "expand" of itself: clicking it would reparent the same
|
|
7
|
+
* card into a *second*, nested modal, leaving the first one open but empty
|
|
8
|
+
* (found via exactly that — every card kept its expand icon once expanded).
|
|
9
|
+
*/
|
|
10
|
+
export declare function openChartExpandModal(card: HTMLElement, title: string, triggerBtn?: HTMLButtonElement): void;
|
|
11
|
+
export interface ChartCanvasColors {
|
|
12
|
+
text: string;
|
|
13
|
+
textMuted: string;
|
|
14
|
+
border: string;
|
|
15
|
+
bg: string;
|
|
16
|
+
bgHover: string;
|
|
17
|
+
/** Subtle bar/row "track" background (full-extent backdrop behind a value bar) — deliberately much softer than `border`, which reads as a visible grey line/fill, not a backdrop. */
|
|
18
|
+
track: string;
|
|
19
|
+
warnBorder: string;
|
|
20
|
+
}
|
|
21
|
+
/** Resolve `--wmap-*` custom properties to concrete color strings for canvas
|
|
22
|
+
* drawing. Call once per draw (not cached) so a live theme change is picked
|
|
23
|
+
* up on the next redraw, matching `resolveCanvasTheme`'s own contract. */
|
|
24
|
+
export declare function resolveChartCanvasColors(el: HTMLElement): ChartCanvasColors;
|
|
25
|
+
export declare const PADDING = 12;
|
|
26
|
+
export declare const VALUE_WIDTH = 100;
|
|
27
|
+
export declare function observeResize(el: HTMLElement, onResize: () => void): {
|
|
28
|
+
disconnect: () => void;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Sets `card`'s own min-height so `body` can actually display
|
|
32
|
+
* `contentHeight` px of content without `body`'s own `overflow-y: auto`
|
|
33
|
+
* (see `cardShell`) silently turning any shortfall into a scrollbar.
|
|
34
|
+
* Computed from `card`'s *live* chrome overhead (heading row, hint text,
|
|
35
|
+
* padding) — not a guessed constant passed in from elsewhere, so it can
|
|
36
|
+
* never drift out of sync with what the panel actually needs (a fixed
|
|
37
|
+
* `card.style.minHeight` guessed once by the composer, as `analysisTab.ts`
|
|
38
|
+
* used to do, drifts the moment hint text length or content height changes).
|
|
39
|
+
*
|
|
40
|
+
* Always *sets* the value fresh, never just grows it — `card`'s container
|
|
41
|
+
* isn't always monotonically growing. The chart-expand modal
|
|
42
|
+
* (`openChartExpandModal`) reparents this same card into a *resizable* box
|
|
43
|
+
* (drag-resize, maximize/restore); a "only grow, never shrink" policy would
|
|
44
|
+
* leave a stale, too-large min-height stuck from a bigger prior size after
|
|
45
|
+
* the modal shrinks, forcing the exact overflow this function exists to
|
|
46
|
+
* prevent. Recomputing from `card`'s current live overhead every call is
|
|
47
|
+
* self-correcting in both directions and converges in one extra resize-
|
|
48
|
+
* observer pass (chrome overhead doesn't change with `card`'s own height,
|
|
49
|
+
* so the fixed point is reached immediately, not oscillating).
|
|
50
|
+
*
|
|
51
|
+
* The Analysis tab composes several of these cards into one CSS Grid row
|
|
52
|
+
* per section (`makeChartGridWrap`); nothing about that grid's own row
|
|
53
|
+
* auto-sizing knows a card needs more room unless the card itself asks for
|
|
54
|
+
* it via `min-height` — a `position: absolute` canvas (`applyCanvasFlow`) is
|
|
55
|
+
* out of normal flow and invisible to it, and even an in-flow one won't ask
|
|
56
|
+
* on its own before this runs.
|
|
57
|
+
*
|
|
58
|
+
* Shared by two different content-sizing patterns:
|
|
59
|
+
* - `chartFillHeight` below: canvas fills whatever's left in `body`.
|
|
60
|
+
* - Panels with a fixed row cap and their own deliberate inner scroll area
|
|
61
|
+
* for the rest (`barPanel.ts`, `boxplot.ts`, `binCluster.ts`) call this
|
|
62
|
+
* directly with their visible-rows height.
|
|
63
|
+
*/
|
|
64
|
+
export declare function growCardToFitContent(card: HTMLElement, body: HTMLElement, contentHeight: number): void;
|
|
65
|
+
/**
|
|
66
|
+
* Canvas height for a "fill the body" chart: siblings already in `body`
|
|
67
|
+
* above the canvas (a stats label, a legend, ...) get their own natural
|
|
68
|
+
* height, and the canvas gets whatever's left — floored at `minHeight` so
|
|
69
|
+
* it never renders squashed below its own readable minimum. See
|
|
70
|
+
* `growCardToFitContent` above for how `card` is kept large enough to
|
|
71
|
+
* actually deliver that floor.
|
|
72
|
+
*/
|
|
73
|
+
export declare function chartFillHeight(card: HTMLElement, body: HTMLElement, canvas: HTMLCanvasElement, minHeight: number): number;
|
|
74
|
+
export declare function applyCanvasFlow(canvas: HTMLCanvasElement, topOffset?: number): void;
|
|
75
|
+
/**
|
|
76
|
+
* Format a bare numeric value (no unit) for chart labels/tooltips —
|
|
77
|
+
* callers append their own unit string separately when they have one. Uses
|
|
78
|
+
* `fmt`'s engineering-notation fallback (fixed decimal for [0.1, 9999],
|
|
79
|
+
* `NEexp` outside that range) rather than a plain `.toFixed(2)`, which
|
|
80
|
+
* silently collapsed small-scale measurements (e.g. a 33.3 pA mean, as
|
|
81
|
+
* `3.33e-11`) to a misleading "0.00".
|
|
82
|
+
*/
|
|
83
|
+
export declare function formatValue(v: number): string;
|
|
84
|
+
/**
|
|
85
|
+
* Shared axis formatter for a numeric chart axis: one SI scale chosen from
|
|
86
|
+
* the largest-magnitude endpoint, applied to every tick, with the scaled
|
|
87
|
+
* unit returned once as the axis label (e.g. ticks "861 · 1040 · 1220"
|
|
88
|
+
* with label "(µA)") — the same `fmtColorbarAxis` contract the map's
|
|
89
|
+
* colorbar uses, so axes never show raw exponent soup like "861E-6" next
|
|
90
|
+
* to a tooltip that says "861 µA". Falls back to `formatValue` ticks and
|
|
91
|
+
* no label when the test has no unit.
|
|
92
|
+
*/
|
|
93
|
+
export declare function makeAxisFormat(vRef: number, unit: string | undefined): {
|
|
94
|
+
tick: (v: number) => string;
|
|
95
|
+
unitLabel: string;
|
|
96
|
+
};
|
|
97
|
+
/** Draw a small "(unit)" label at (x, y), restoring the context's text state afterward. */
|
|
98
|
+
export declare function drawAxisUnit(ctx: CanvasRenderingContext2D, unit: string, x: number, y: number, color: string): void;
|
|
99
|
+
export declare function saveCanvasPng(canvas: HTMLCanvasElement, filenameStem: string, onSaveImage?: SaveImageHandler): void;
|
|
100
|
+
export interface CardShell {
|
|
101
|
+
card: HTMLElement;
|
|
102
|
+
heading: HTMLElement;
|
|
103
|
+
controlsRow: HTMLElement;
|
|
104
|
+
body: HTMLElement;
|
|
105
|
+
}
|
|
106
|
+
export declare function cardShell(title: string, onSaveImage?: SaveImageHandler): CardShell;
|
|
107
|
+
export declare function makeSegmented(options: Array<[value: string, label: string]>, current: string, onChange: (value: string) => void): HTMLElement;
|
|
108
|
+
export declare function makeBackButton(onBack: () => void): HTMLButtonElement;
|
|
109
|
+
export declare function renderEmptyState(body: HTMLElement, message: string, styleOverrides?: Partial<CSSStyleDeclaration>): void;
|
|
110
|
+
export interface TestSelectItem {
|
|
111
|
+
testNumber: number;
|
|
112
|
+
name?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Below `MENU_SEARCH_THRESHOLD` tests this is a plain native `<select>`. Past
|
|
116
|
+
* it, a long native dropdown becomes hard to scan, so it's replaced by a
|
|
117
|
+
* button + filterable popup list (`makeSearchableTestCombo` below) — sharing
|
|
118
|
+
* `makeMenuSearchBox` with the plot-mode test-value cascade submenu
|
|
119
|
+
* (toolbar.ts's `buildModeMenuEl`) so both long test lists get the same
|
|
120
|
+
* filter-as-you-type box instead of two independent implementations. Either
|
|
121
|
+
* shape exposes a settable `.value` (string of the testNumber, same as a real
|
|
122
|
+
* `<select>`) so callers (boxplot/histogram/scatter, syncing from an external
|
|
123
|
+
* click-through) don't need to know which one they got.
|
|
124
|
+
*/
|
|
125
|
+
export declare function makeTestSelect(testOptions: readonly TestSelectItem[], selected: number | null, onChange: (testNumber: number) => void, opts?: {
|
|
126
|
+
maxWidth?: string;
|
|
127
|
+
emptyText?: string;
|
|
128
|
+
}): HTMLElement & {
|
|
129
|
+
value: string;
|
|
130
|
+
};
|
|
131
|
+
export interface WaferSelectItem {
|
|
132
|
+
label?: string;
|
|
133
|
+
}
|
|
134
|
+
export declare function makeWaferSelect(items: readonly WaferSelectItem[], selectedIndex: number | null, onChange: (index: number | null) => void, opts?: {
|
|
135
|
+
maxWidth?: string;
|
|
136
|
+
allLabel?: string;
|
|
137
|
+
}): HTMLSelectElement;
|
|
138
|
+
export declare function makeToggle(labelText: string, checked: boolean, onChange: (v: boolean) => void): HTMLLabelElement;
|
|
139
|
+
export declare function makeLabeledSelect(labelText: string, options: readonly {
|
|
140
|
+
value: string;
|
|
141
|
+
label: string;
|
|
142
|
+
}[], selected: string, onChange: (value: string) => void, opts?: {
|
|
143
|
+
maxWidth?: string;
|
|
144
|
+
}): HTMLLabelElement;
|
|
145
|
+
export declare function makeChartGridWrap(): HTMLDivElement;
|
|
146
|
+
/** Small positioned hover tooltip, matching the pattern each ported panel builds for itself. */
|
|
147
|
+
export declare function makeTooltip(card: HTMLElement): HTMLElement;
|
|
148
|
+
/**
|
|
149
|
+
* Position a `makeTooltip` element from a mousemove event, flipping to the
|
|
150
|
+
* opposite side of the cursor when the default placement would overflow
|
|
151
|
+
* `card`'s own bounds (the tooltip is an absolutely-positioned child of
|
|
152
|
+
* `card`, so staying within its rect keeps it on-screen without needing the
|
|
153
|
+
* window's own bounds).
|
|
154
|
+
*/
|
|
155
|
+
export declare function positionChartTooltip(tooltip: HTMLElement, card: HTMLElement, clientX: number, clientY: number): void;
|
|
156
|
+
//# sourceMappingURL=chartShell.d.ts.map
|
|
@@ -0,0 +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`}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type FacetItem } from '../../stats/facets.js';
|
|
2
|
+
import type { Die } from '../../core/dies.js';
|
|
3
|
+
import type { TestDef } from '../../renderer/buildWaferMap.js';
|
|
4
|
+
import { type SaveImageHandler } from './chartShell.js';
|
|
5
|
+
type CorrelationItem = FacetItem & {
|
|
6
|
+
dies?: Die[];
|
|
7
|
+
label?: string;
|
|
8
|
+
};
|
|
9
|
+
export interface CorrelationPanelOptions {
|
|
10
|
+
title?: string;
|
|
11
|
+
items: CorrelationItem[];
|
|
12
|
+
testDefs: TestDef[];
|
|
13
|
+
colorScheme?: string;
|
|
14
|
+
onSaveImage?: SaveImageHandler;
|
|
15
|
+
/**
|
|
16
|
+
* When the Analysis tab's "Group by" is active, this panel gets its own
|
|
17
|
+
* "Group: <value> ▾" restrict-to-one-group dropdown (matching capability's
|
|
18
|
+
* pattern exactly) — the matrix is always computed for exactly one group's
|
|
19
|
+
* dies, never pooled across groups. `items` above is ignored when `groups`
|
|
20
|
+
* is provided. Absent ⇒ today's plain ungrouped behavior.
|
|
21
|
+
*/
|
|
22
|
+
groups?: {
|
|
23
|
+
key: string;
|
|
24
|
+
items: CorrelationItem[];
|
|
25
|
+
}[];
|
|
26
|
+
/** Clicking a non-diagonal cell calls this — the Analysis tab wires it to drive the scatter panel's X/Y in place. */
|
|
27
|
+
onSelectPair?: (xTestNumber: number, yTestNumber: number) => void;
|
|
28
|
+
}
|
|
29
|
+
export interface CorrelationPanelHandle {
|
|
30
|
+
card: HTMLElement;
|
|
31
|
+
destroy: () => void;
|
|
32
|
+
}
|
|
33
|
+
export declare function renderCorrelationPanel(options: CorrelationPanelOptions): CorrelationPanelHandle;
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=correlation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{buildCorrelationMatrix as ft,filterCorrelationMatrix as ut}from"../../stats/correlation.js";import{CORRELATION_POSITIVE as et,CORRELATION_NEGATIVE as nt}from"./palette.js";import{buildFacetTable as ht}from"../../stats/facets.js";import{CLR as x}from"../toolbar.js";import{cardShell as mt,observeResize as xt,makeTooltip as gt,positionChartTooltip as bt,makeLabeledSelect as yt,makeWaferSelect as Mt,renderEmptyState as Lt,resolveChartCanvasColors as vt}from"./chartShell.js";const st=5,ot=100,lt=20;function it(T){const c=T.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(c)return[+c[1],+c[2],+c[3]];const r=T.trim().replace("#","");return r.length===6?[parseInt(r.slice(0,2),16),parseInt(r.slice(2,4),16),parseInt(r.slice(4,6),16)]:null}function It(T,c,r){const k=it(T);if(!k)return T;const P=Math.round(c[0]+(k[0]-c[0])*r),u=Math.round(c[1]+(k[1]-c[1])*r),A=Math.round(c[2]+(k[2]-c[2])*r);return`rgb(${P},${u},${A})`}export function renderCorrelationPanel(T){const{title:c="Test correlation matrix",items:r,testDefs:k,onSaveImage:P,groups:u,onSelectPair:A}=T,{card:$,body:w,controlsRow:V}=mt(c,P);w.style.overflowX="auto",$.style.alignSelf="start";let F=lt,U=u&&u.length>0?u[0].key:void 0,j=null;const H=document.createElement("label");H.textContent="Max tests:",H.title="Cap on how many tests the matrix includes (strongest correlations kept first)",Object.assign(H.style,{color:x.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const L=document.createElement("input");L.type="number",L.min=String(st),L.max=String(ot),L.value=String(F),Object.assign(L.style,{width:"52px",fontSize:"12px",padding:"2px 4px",background:x.menuBg,color:x.value,border:`1px solid ${x.menuBorder}`,borderRadius:"3px"}),L.addEventListener("change",()=>{const e=Math.max(st,Math.min(ot,parseInt(L.value,10)||lt));L.value=String(e),e!==F&&(F=e,G())}),H.appendChild(L),V.appendChild(H),u&&u.length>0?V.appendChild(yt("Group:",u.map(e=>({value:e.key,label:e.key})),U??"",e=>{U=e,G()})):r.length>1&&V.appendChild(Mt(r,j,e=>{j=e,G()}));function rt(){return u&&u.length>0?u.find(e=>e.key===U)?.items??[]:j!==null?r[j]?[r[j]]:[]:r}const B=document.createElement("div");Object.assign(B.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),$.insertBefore(B,w);const W=window.devicePixelRatio||1,S=gt($);let q=()=>{};function at(e,o,a,f,I){if(B.innerHTML="",I.length>0){const b=document.createElement("div");b.textContent=`\u26A0 Mixed ${I.join(", ")} within this set \u2014 correlations may be misleading (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,Object.assign(b.style,{color:x.warnText,background:x.warnBg,border:`1px solid ${x.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px"}),B.appendChild(b)}const i=document.createElement("span");Object.assign(i.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:x.label,fontSize:"11px",flexWrap:"wrap"});const C=document.createElement("span");C.textContent="Pearson r \xB7 click a cell to view that pair in scatter \xB7",i.appendChild(C);const d=document.createElement("span");Object.assign(d.style,{display:"inline-flex",alignItems:"center",gap:"4px"});const O=document.createElement("span");O.textContent="\u22121";const g=document.createElement("span");Object.assign(g.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:"2px",border:`1px solid ${x.menuBorder}`,background:`linear-gradient(to right, ${nt}, ${x.menuBg}, ${et})`});const _=document.createElement("span");_.textContent="+1",d.append(O,g,_),i.appendChild(d),B.appendChild(i);const E=document.createElement("span");if(Object.assign(E.style,{color:x.value,fontSize:"12px",fontWeight:"500"}),e===0&&o===0)E.textContent=f?`No significant correlations found \u2014 strongest pair: ${f.xLabel} \u2194 ${f.yLabel} (r = ${f.r.toFixed(2)})`:"No significant correlations found";else{const b=[];e>0&&b.push(`${e} strong (|r| \u2265 0.7)`),o>0&&b.push(`${o} moderate (0.4\u20130.7)`);const X=e+o;let z=b.join(", ")+` pair${X!==1?"s":""} found`;a>0&&(z+=` \xB7 ${a} weak pair${a!==1?"s":""} not shown`),E.textContent=z}B.appendChild(E)}function ct(e){if(w.innerHTML="",e.tests.length<2)return Lt(w,"Need at least two parametric tests for a correlation matrix."),()=>{};const o=document.createElement("canvas");o.style.display="block",o.style.cursor="default",w.appendChild(o);const a=e.tests.length,f=s=>s.label.split(" (#")[0],I=Math.min(14,Math.max(...e.tests.map(s=>f(s).length))),i=I*6.5+8,C=10,d=Math.round(C*6.5*Math.sin(Math.PI/4))+14,O=14,g=26;let _=-1,E=-1;function b(s){const l=Math.max(0,s-i);return Math.max(O,Math.min(g,Math.floor(l/a)))}const X=new Map;for(const s of e.cells){let l=X.get(s.yIndex);l||(l=[],X.set(s.yIndex,l)),l.push(s)}function z(){const s=vt($),l=it(s.bg)??[255,255,255],p=w.clientWidth,n=b(p),N=n*a,v=d+n*a+4,y=i+N;o.width=Math.max(1,Math.floor(y*W)),o.height=Math.max(1,Math.floor(v*W)),o.style.width=`${y}px`,o.style.height=`${v}px`;const t=o.getContext("2d");t.setTransform(W,0,0,W,0,0),t.clearRect(0,0,y,v),t.font="10px system-ui, sans-serif",t.fillStyle=s.text,e.tests.forEach((h,m)=>{const M=f(h),R=M.length>C?`${M.slice(0,C-1)}\u2026`:M,J=i+m*n+n/2;t.save(),t.translate(J,d-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=m===_?s.text:s.textMuted,t.fillText(R,0,0),t.restore()}),e.tests.forEach((h,m)=>{const M=f(h),R=d+m*n,J=R+n/2;t.font="10px system-ui, sans-serif",t.textAlign="right",t.textBaseline="middle",t.fillStyle=m===E?s.text:s.textMuted,t.fillText(M.length>I?`${M.slice(0,I-1)}\u2026`:M,i-4,J),(X.get(m)??[]).forEach(Z=>{const K=Z.xIndex,Y=i+K*n,pt=K===_&&m===E,tt=K===m,D=Z.r;if(D===null){t.fillStyle=s.border,t.globalAlpha=.4,t.fillRect(Y+1,R+1,n-2,n-2),t.globalAlpha=1;return}tt?t.fillStyle=s.bgHover:t.fillStyle=It(D>=0?et:nt,l,Math.abs(D)),t.fillRect(Y+1,R+1,n-2,n-2),n>=28&&!tt&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Math.abs(D)>.6?s.bg:s.text,t.textAlign="center",t.textBaseline="middle",t.fillText(D.toFixed(2),Y+n/2,R+n/2)),pt&&(t.strokeStyle=s.text,t.lineWidth=2,t.strokeRect(Y+1,R+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=s.bg,t.lineWidth=1;for(let h=0;h<=a;h++){const m=i+h*n;t.beginPath(),t.moveTo(m,d),t.lineTo(m,d+a*n),t.stroke();const M=d+h*n;t.beginPath(),t.moveTo(i,M),t.lineTo(i+a*n,M),t.stroke()}}function Q(s){const l=o.getBoundingClientRect(),p=w.clientWidth,n=b(p),N=(s.clientX-l.left)*(o.width/W/l.width),v=(s.clientY-l.top)*(o.height/W/l.height),y=Math.floor((N-i)/n),t=Math.floor((v-d)/n);return y>=0&&y<a&&t>=0&&t<a?{xi:y,yi:t}:null}return o.addEventListener("mousemove",s=>{const l=Q(s);if(!l){S.style.display="none",o.style.cursor="default";return}const{xi:p,yi:n}=l,N=p===n;o.style.cursor=N||!A?"default":"pointer";const v=e.cells.find(h=>h.xIndex===p&&h.yIndex===n),y=e.tests[p].label,t=e.tests[n].label;N?S.innerHTML=`<strong>${y}</strong>`:v?.r!==null&&v?.r!==void 0?S.innerHTML=`<strong>${f(e.tests[n])}</strong> (#${e.tests[n].testNumber}) vs <strong>${f(e.tests[p])}</strong> (#${e.tests[p].testNumber})<br>r = ${v.r.toFixed(4)}${A?"<br><em>click to view in scatter</em>":""}`:S.innerHTML=`${t} vs ${y}<br><em>insufficient data</em>`,S.style.display="block",bt(S,$,s.clientX,s.clientY)}),o.addEventListener("mouseleave",()=>{S.style.display="none"}),o.addEventListener("click",s=>{const l=Q(s);if(!l||!A)return;const{xi:p,yi:n}=l;p!==n&&(_=p,E=n,z(),A(e.tests[p].testNumber,e.tests[n].testNumber))}),z}function G(){const e=rt(),o=e.flatMap(g=>g.dies??[]),a=ft(o,k),{matrix:f,strongPairs:I,moderatePairs:i,hiddenWeakPairs:C,strongestPair:d}=ut(a,{minTests:6,maxTests:F}),O=ht(e,{facetableOnly:!0}).filter(g=>g.splittable).map(g=>g.label);at(I,i,C,d,O),q=ct(f),q()}const dt=xt($,()=>q());return G(),{card:$,destroy:()=>dt.disconnect()}}
|