@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.
Files changed (138) hide show
  1. package/CHANGELOG.md +751 -0
  2. package/LICENSE +21 -0
  3. package/README.md +79 -0
  4. package/dist/index.d.ts +4 -0
  5. package/dist/index.js +1 -0
  6. package/dist/packages/canvas-adapter/canvasTheme.d.ts +28 -0
  7. package/dist/packages/canvas-adapter/canvasTheme.js +1 -0
  8. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +52 -0
  9. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -0
  10. package/dist/packages/canvas-adapter/charts/binCluster.d.ts +17 -0
  11. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -0
  12. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +45 -0
  13. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -0
  14. package/dist/packages/canvas-adapter/charts/capability.d.ts +41 -0
  15. package/dist/packages/canvas-adapter/charts/capability.js +1 -0
  16. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +156 -0
  17. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -0
  18. package/dist/packages/canvas-adapter/charts/correlation.d.ts +35 -0
  19. package/dist/packages/canvas-adapter/charts/correlation.js +1 -0
  20. package/dist/packages/canvas-adapter/charts/histogram.d.ts +30 -0
  21. package/dist/packages/canvas-adapter/charts/histogram.js +1 -0
  22. package/dist/packages/canvas-adapter/charts/index.d.ts +16 -0
  23. package/dist/packages/canvas-adapter/charts/index.js +1 -0
  24. package/dist/packages/canvas-adapter/charts/palette.d.ts +26 -0
  25. package/dist/packages/canvas-adapter/charts/palette.js +1 -0
  26. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +18 -0
  27. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -0
  28. package/dist/packages/canvas-adapter/charts/scatter.d.ts +37 -0
  29. package/dist/packages/canvas-adapter/charts/scatter.js +1 -0
  30. package/dist/packages/canvas-adapter/icons.d.ts +2 -0
  31. package/dist/packages/canvas-adapter/icons.js +42 -0
  32. package/dist/packages/canvas-adapter/index.d.ts +12 -0
  33. package/dist/packages/canvas-adapter/index.js +1 -0
  34. package/dist/packages/canvas-adapter/insightsTab.d.ts +76 -0
  35. package/dist/packages/canvas-adapter/insightsTab.js +1 -0
  36. package/dist/packages/canvas-adapter/metadataBadge.d.ts +26 -0
  37. package/dist/packages/canvas-adapter/metadataBadge.js +1 -0
  38. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +181 -0
  39. package/dist/packages/canvas-adapter/renderWaferGallery.js +20 -0
  40. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +316 -0
  41. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -0
  42. package/dist/packages/canvas-adapter/summaryPanel.d.ts +262 -0
  43. package/dist/packages/canvas-adapter/summaryPanel.js +3 -0
  44. package/dist/packages/canvas-adapter/toCanvas.d.ts +106 -0
  45. package/dist/packages/canvas-adapter/toCanvas.js +1 -0
  46. package/dist/packages/canvas-adapter/toolbar.d.ts +496 -0
  47. package/dist/packages/canvas-adapter/toolbar.js +9 -0
  48. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +2 -0
  49. package/dist/packages/canvas-adapter/userGuideHtml.js +968 -0
  50. package/dist/packages/canvas-adapter/version.d.ts +3 -0
  51. package/dist/packages/canvas-adapter/version.js +1 -0
  52. package/dist/packages/core/aggregates.d.ts +54 -0
  53. package/dist/packages/core/aggregates.js +1 -0
  54. package/dist/packages/core/classify.d.ts +24 -0
  55. package/dist/packages/core/classify.js +1 -0
  56. package/dist/packages/core/dies.d.ts +92 -0
  57. package/dist/packages/core/dies.js +1 -0
  58. package/dist/packages/core/index.d.ts +13 -0
  59. package/dist/packages/core/index.js +1 -0
  60. package/dist/packages/core/inference/grid.d.ts +35 -0
  61. package/dist/packages/core/inference/grid.js +1 -0
  62. package/dist/packages/core/inference/index.d.ts +7 -0
  63. package/dist/packages/core/inference/index.js +1 -0
  64. package/dist/packages/core/inference/pitch.d.ts +26 -0
  65. package/dist/packages/core/inference/pitch.js +1 -0
  66. package/dist/packages/core/inference/wafer.d.ts +30 -0
  67. package/dist/packages/core/inference/wafer.js +1 -0
  68. package/dist/packages/core/metadata.d.ts +31 -0
  69. package/dist/packages/core/metadata.js +1 -0
  70. package/dist/packages/core/probe.d.ts +16 -0
  71. package/dist/packages/core/probe.js +1 -0
  72. package/dist/packages/core/reticle.d.ts +68 -0
  73. package/dist/packages/core/reticle.js +1 -0
  74. package/dist/packages/core/transforms.d.ts +142 -0
  75. package/dist/packages/core/transforms.js +1 -0
  76. package/dist/packages/core/utils.d.ts +60 -0
  77. package/dist/packages/core/utils.js +1 -0
  78. package/dist/packages/core/wafer.d.ts +45 -0
  79. package/dist/packages/core/wafer.js +1 -0
  80. package/dist/packages/renderer/buildView.d.ts +420 -0
  81. package/dist/packages/renderer/buildView.js +1 -0
  82. package/dist/packages/renderer/buildWaferMap.d.ts +543 -0
  83. package/dist/packages/renderer/buildWaferMap.js +1 -0
  84. package/dist/packages/renderer/colorMap.d.ts +46 -0
  85. package/dist/packages/renderer/colorMap.js +1 -0
  86. package/dist/packages/renderer/colorSchemes.d.ts +40 -0
  87. package/dist/packages/renderer/colorSchemes.js +1 -0
  88. package/dist/packages/renderer/fmt.d.ts +30 -0
  89. package/dist/packages/renderer/fmt.js +1 -0
  90. package/dist/packages/renderer/index.d.ts +7 -0
  91. package/dist/packages/renderer/index.js +1 -0
  92. package/dist/packages/stats/analyzeWaferLot.d.ts +5 -0
  93. package/dist/packages/stats/analyzeWaferLot.js +1 -0
  94. package/dist/packages/stats/analyzeWaferMap.d.ts +13 -0
  95. package/dist/packages/stats/analyzeWaferMap.js +1 -0
  96. package/dist/packages/stats/binPareto.d.ts +49 -0
  97. package/dist/packages/stats/binPareto.js +1 -0
  98. package/dist/packages/stats/boxplot.d.ts +39 -0
  99. package/dist/packages/stats/boxplot.js +1 -0
  100. package/dist/packages/stats/capability.d.ts +55 -0
  101. package/dist/packages/stats/capability.js +1 -0
  102. package/dist/packages/stats/clusterDetection.d.ts +18 -0
  103. package/dist/packages/stats/clusterDetection.js +1 -0
  104. package/dist/packages/stats/correlation.d.ts +55 -0
  105. package/dist/packages/stats/correlation.js +1 -0
  106. package/dist/packages/stats/facets.d.ts +74 -0
  107. package/dist/packages/stats/facets.js +1 -0
  108. package/dist/packages/stats/filterFindings.d.ts +9 -0
  109. package/dist/packages/stats/filterFindings.js +1 -0
  110. package/dist/packages/stats/findingsNarrative.d.ts +3 -0
  111. package/dist/packages/stats/findingsNarrative.js +1 -0
  112. package/dist/packages/stats/histogram.d.ts +42 -0
  113. package/dist/packages/stats/histogram.js +1 -0
  114. package/dist/packages/stats/index.d.ts +27 -0
  115. package/dist/packages/stats/index.js +1 -0
  116. package/dist/packages/stats/math.d.ts +10 -0
  117. package/dist/packages/stats/math.js +1 -0
  118. package/dist/packages/stats/patternClassification.d.ts +107 -0
  119. package/dist/packages/stats/patternClassification.js +1 -0
  120. package/dist/packages/stats/regions.d.ts +57 -0
  121. package/dist/packages/stats/regions.js +1 -0
  122. package/dist/packages/stats/renderFindingsReport.d.ts +7 -0
  123. package/dist/packages/stats/renderFindingsReport.js +41 -0
  124. package/dist/packages/stats/renderSummaryReport.d.ts +60 -0
  125. package/dist/packages/stats/renderSummaryReport.js +68 -0
  126. package/dist/packages/stats/reportHtml.d.ts +42 -0
  127. package/dist/packages/stats/reportHtml.js +301 -0
  128. package/dist/packages/stats/scatter.d.ts +24 -0
  129. package/dist/packages/stats/scatter.js +1 -0
  130. package/dist/packages/stats/types.d.ts +269 -0
  131. package/dist/packages/stats/types.js +1 -0
  132. package/dist/packages/stats/yield.d.ts +56 -0
  133. package/dist/packages/stats/yield.js +1 -0
  134. package/dist/packages/worker/index.d.ts +44 -0
  135. package/dist/packages/worker/index.js +1 -0
  136. package/dist/packages/worker/wafermap.worker.d.ts +44 -0
  137. package/dist/packages/worker/wafermap.worker.js +1 -0
  138. 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
+ [![CI](https://github.com/wafertools/wafermap/actions/workflows/ci.yml/badge.svg)](https://github.com/wafertools/wafermap/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@wafertools/wafermap.svg)](https://www.npmjs.com/package/@wafertools/wafermap)
5
+ ![runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)
6
+ ![bundle](https://img.shields.io/badge/core%20min%2Bgz-~40%20kB-blue)
7
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./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/`.
@@ -0,0 +1,4 @@
1
+ export * from './packages/core/index.js';
2
+ export * from './packages/renderer/index.js';
3
+ export * from './packages/stats/index.js';
4
+ //# sourceMappingURL=index.d.ts.map
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()}}