@wafertools/wafermap 0.30.1 → 0.30.3

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 (94) hide show
  1. package/AGENTS.md +71 -11
  2. package/CHANGELOG.md +464 -8
  3. package/README.md +6 -3
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +48 -0
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -0
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +4 -1
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +4 -1
  9. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +81 -7
  12. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  13. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  14. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  15. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  16. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  18. package/dist/packages/canvas-adapter/charts/sweep.d.ts +31 -0
  19. package/dist/packages/canvas-adapter/charts/sweep.js +1 -0
  20. package/dist/packages/canvas-adapter/charts/trend.d.ts +3 -1
  21. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  22. package/dist/packages/canvas-adapter/chunked.d.ts +45 -0
  23. package/dist/packages/canvas-adapter/chunked.js +1 -0
  24. package/dist/packages/canvas-adapter/dieList.js +2 -2
  25. package/dist/packages/canvas-adapter/drilldown.d.ts +23 -0
  26. package/dist/packages/canvas-adapter/drilldown.js +1 -0
  27. package/dist/packages/canvas-adapter/icons.js +1 -1
  28. package/dist/packages/canvas-adapter/insightsTab.d.ts +55 -3
  29. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  30. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +55 -0
  31. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  32. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  33. package/dist/packages/canvas-adapter/summaryPanel.d.ts +55 -9
  34. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  35. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  36. package/dist/packages/canvas-adapter/toolbar.d.ts +30 -2
  37. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  38. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  39. package/dist/packages/canvas-adapter/userGuideHtml.js +126 -3
  40. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  41. package/dist/packages/canvas-adapter/version.js +1 -1
  42. package/dist/packages/canvas-adapter/warnings.js +1 -1
  43. package/dist/packages/core/aggregates.js +1 -1
  44. package/dist/packages/core/utils.d.ts +55 -0
  45. package/dist/packages/core/utils.js +1 -1
  46. package/dist/packages/renderer/axisTicks.d.ts +32 -0
  47. package/dist/packages/renderer/axisTicks.js +1 -0
  48. package/dist/packages/renderer/binColors.d.ts +14 -0
  49. package/dist/packages/renderer/binColors.js +1 -1
  50. package/dist/packages/renderer/buildView.d.ts +11 -37
  51. package/dist/packages/renderer/buildView.js +1 -1
  52. package/dist/packages/renderer/buildWaferMap.d.ts +58 -1
  53. package/dist/packages/renderer/buildWaferMap.js +1 -1
  54. package/dist/packages/renderer/deprecate.d.ts +13 -5
  55. package/dist/packages/renderer/deprecate.js +1 -1
  56. package/dist/packages/renderer/derivedTests/apply.d.ts +85 -0
  57. package/dist/packages/renderer/derivedTests/apply.js +1 -0
  58. package/dist/packages/renderer/derivedTests/evaluate.d.ts +48 -0
  59. package/dist/packages/renderer/derivedTests/evaluate.js +1 -0
  60. package/dist/packages/renderer/derivedTests/parser.d.ts +148 -0
  61. package/dist/packages/renderer/derivedTests/parser.js +2 -0
  62. package/dist/packages/renderer/fmt.d.ts +24 -2
  63. package/dist/packages/renderer/fmt.js +1 -1
  64. package/dist/packages/renderer/index.d.ts +1 -0
  65. package/dist/packages/renderer/index.js +1 -1
  66. package/dist/packages/renderer/spec.d.ts +49 -0
  67. package/dist/packages/renderer/spec.js +1 -0
  68. package/dist/packages/renderer/testLabel.d.ts +154 -0
  69. package/dist/packages/renderer/testLabel.js +1 -0
  70. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  71. package/dist/packages/stats/capability.d.ts +60 -0
  72. package/dist/packages/stats/capability.js +1 -1
  73. package/dist/packages/stats/correlation.d.ts +33 -0
  74. package/dist/packages/stats/correlation.js +1 -1
  75. package/dist/packages/stats/deprecated.d.ts +3 -0
  76. package/dist/packages/stats/deprecated.js +1 -1
  77. package/dist/packages/stats/findingsNarrative.js +1 -1
  78. package/dist/packages/stats/index.d.ts +2 -2
  79. package/dist/packages/stats/index.js +1 -1
  80. package/dist/packages/stats/math.d.ts +27 -2
  81. package/dist/packages/stats/math.js +1 -1
  82. package/dist/packages/stats/mergeTestDefs.js +1 -1
  83. package/dist/packages/stats/renderFindingsReport.js +8 -28
  84. package/dist/packages/stats/renderSummaryReport.js +22 -31
  85. package/dist/packages/stats/reportHtml.d.ts +27 -0
  86. package/dist/packages/stats/reportHtml.js +35 -14
  87. package/dist/packages/stats/sweep.d.ts +178 -0
  88. package/dist/packages/stats/sweep.js +1 -0
  89. package/dist/packages/stats/sweepXFromName.d.ts +27 -0
  90. package/dist/packages/stats/sweepXFromName.js +1 -0
  91. package/dist/packages/stats/testPassRate.js +1 -1
  92. package/dist/packages/stats/types.d.ts +28 -1
  93. package/llms.txt +14 -8
  94. package/package.json +2 -2
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![CI and deploy](https://github.com/wafertools/wafermap/actions/workflows/deploy.yml/badge.svg)](https://github.com/wafertools/wafermap/actions/workflows/deploy.yml)
6
6
  [![npm](https://img.shields.io/npm/v/@wafertools/wafermap.svg)](https://www.npmjs.com/package/@wafertools/wafermap)
7
7
  ![runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)
8
- ![bundle](https://img.shields.io/badge/data%20layer%20min%2Bgz-~53%20kB-blue)
8
+ ![bundle](https://img.shields.io/badge/data%20layer%20min%2Bgz-~59%20kB-blue)
9
9
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
10
10
 
11
11
  <img src="docs/images/hero-test-values.png" alt="wafermap demo" style="max-width:640px; display:block; margin:8px 0;" />
@@ -13,7 +13,7 @@
13
13
  Browser-first wafer map visualization for semiconductor test data.
14
14
 
15
15
  **Zero runtime dependencies.** Pure ES modules with TypeScript types — works in React,
16
- Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~53 kB
16
+ Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~59 kB
17
17
  min+gz; the interactive renderer is larger, and its chart suite and in-app guide are
18
18
  loaded on demand rather than shipped up front — [measured sizes](docs/performance.md).
19
19
 
@@ -32,6 +32,9 @@ wafermap renders interactive wafer maps from semiconductor prober output. Hard b
32
32
  - `renderWaferMap` — interactive canvas map with toolbar, zoom/pan, tooltips, die selection, and summary panel
33
33
  - `renderWaferGallery` — lot-level card grid with shared controls and click-to-expand
34
34
  - Insights tab (`insights: { enabled: true }`) — an in-toolbar chart suite (yield, per-test pass rate, bin pareto, capability, boxplot, histogram, wafer-to-wafer trend, correlation, scatter) computed from the same wafer/lot data, for one wafer or the whole gallery
35
+ - Derived tests (`derivedTests`) — tests computed per die from other tests (`abs(t[1020] - t[1010])`), which then work everywhere a measured test does, marked † as not measured
36
+ - Parametric sweeps (`insights.sweeps`) — an ordered run of tests read as a response curve, with the crossing and widths between two curves measured; x values given directly or read from the test names, on a linear or log axis
37
+ - Drilldown — right-click selected dies, a wafer card or a wafer's bar to chart just that population, with the population stated on the chart
35
38
  - `analyzeWaferMap` / `analyzeWaferLot` — spatial analysis across rings, quadrants, sectors, and reticle positions; failure cluster detection; lot trend series
36
39
  - Pure ES modules, no server, no runtime dependencies — works in React, Svelte, Vue, plain HTML, or a Web Worker
37
40
 
@@ -72,7 +75,7 @@ export. Add `analyzeWaferMap` for the findings and summary panel, swap in
72
75
 
73
76
  The API reference is long because it documents every option, not because you need
74
77
  them: [tsmap](https://github.com/wafertools/tsmap), a complete cross-platform desktop
75
- application built on this library, imports **15** of its ~100 exports. Read the
78
+ application built on this library, imports **17** of its ~100 exports. Read the
76
79
  [Quick Start](https://wafertools.github.io/wafermap/quickstart/) first and treat the
77
80
  [API reference](https://wafertools.github.io/wafermap/api/) as something to search,
78
81
  not to read.
@@ -0,0 +1,48 @@
1
+ import type { Die } from '../core/dies.js';
2
+ import { type TestDef } from '../renderer/buildWaferMap.js';
3
+ import type { SweepSpec } from '../stats/sweep.js';
4
+ /** One wafer's share of a population. */
5
+ export interface DrilldownItem {
6
+ /** The wafer's display label. */
7
+ label: string;
8
+ dies: Die[];
9
+ waferIndex?: number;
10
+ }
11
+ /** A population a chart can be opened on. */
12
+ export interface DrilldownSource {
13
+ /** One entry per wafer the population spans. A snapshot: the chart keeps
14
+ * these dies whatever happens to the selection afterwards. */
15
+ items: DrilldownItem[];
16
+ /** Who the dies are, read after a count — `"selected on W03"`, `"on W03"`. */
17
+ population: string;
18
+ testDefs: TestDef[] | undefined;
19
+ /** The test on screen where the gesture happened — a chart that shows one
20
+ * test at a time opens on it. */
21
+ activeTest?: number;
22
+ /** Set when the dies are not measured dies (a lot stack's per-position
23
+ * aggregates): every chart is then unavailable, with this as the reason. */
24
+ notMeasuredReason?: string;
25
+ }
26
+ /**
27
+ * Whether any drilldown chart could exist for this data — decides whether a
28
+ * right-click is taken over at all, without loading the drilldown chunk. The
29
+ * distribution charts need a parametric test, a sweep needs to be defined;
30
+ * with neither (a bins-only map) right-click stays the browser's or host's.
31
+ * `targetsFor` in drilldown.ts is the list this summarises.
32
+ */
33
+ export declare function hasDrilldownTargets(testDefs: readonly TestDef[] | undefined, sweeps: readonly SweepSpec[] | undefined): boolean;
34
+ interface WaferFacts {
35
+ /** The wafer's real identity, if it has one — never a positional stand-in,
36
+ * which would read as an ID ("Wafer 3 (no ID)") in a chart title. */
37
+ waferLabel: string | undefined;
38
+ testDefs: TestDef[] | undefined;
39
+ isLotStack?: boolean;
40
+ activeTest?: number;
41
+ waferIndex?: number;
42
+ }
43
+ /** Dies the user selected on one wafer's map. */
44
+ export declare function selectionPopulation(selected: Die[], f: WaferFacts): DrilldownSource;
45
+ /** Every die of one wafer. */
46
+ export declare function waferPopulation(dies: Die[], f: WaferFacts): DrilldownSource;
47
+ export {};
48
+ //# sourceMappingURL=chartPopulation.d.ts.map
@@ -0,0 +1 @@
1
+ import{isParametricTest as s}from"../renderer/buildWaferMap.js";export function hasDrilldownTargets(t,e){return(e?.length??0)>0||(t??[]).some(s)}const n="This map stacks a lot: its dies are per-position aggregates, not measured dies";function r(t,e,a){return{items:[{label:a.waferLabel??"this wafer",dies:t,waferIndex:a.waferIndex}],population:e,testDefs:a.testDefs,activeTest:a.activeTest,notMeasuredReason:a.isLotStack?n:void 0}}export function selectionPopulation(t,e){return r(t,e.waferLabel?`selected on ${e.waferLabel}`:"selected",e)}export function waferPopulation(t,e){return r(t,e.waferLabel?`on ${e.waferLabel}`:"on this wafer",e)}
@@ -1,5 +1,5 @@
1
1
  import type { ChartDatum } from '../../stats/yield.js';
2
- import { type SaveImageHandler } from './chartShell.js';
2
+ import { type SaveImageHandler, type WaferContextMenuHandler } from './chartShell.js';
3
3
  export interface ChartPanel {
4
4
  title: string;
5
5
  data: ChartDatum[];
@@ -59,6 +59,9 @@ export interface ChartPanel {
59
59
  * instead, via `drill.onOpenGroup` — this is never called for those.
60
60
  */
61
61
  onOpen?: (datum: ChartDatum) => void;
62
+ /** Right-click on a leaf row that carries a wafer (`datum.key`) — see
63
+ * `WaferContextMenuHandler`. Not called for a group row. */
64
+ onWaferContextMenu?: WaferContextMenuHandler;
62
65
  /** Document to build this panel's DOM into. Default `document` — pass the
63
66
  * host's own `ownerDocument` when the container might live in a
64
67
  * different document (e.g. a gallery card detached into its own popup
@@ -1 +1 @@
1
- import{SPACE as nt,fontPx as ot,FONT as lt,CLR as it}from"../toolbar.js";import{cardShell as rt,formatValue as st,observeResize as ct,makeTooltip as at,positionChartTooltip as ft,makeBackButton as ht,makeSegmented as dt,growCardToFitContent as ut,resolveChartCanvasColors as mt,PADDING as f,VALUE_WIDTH as z,prepareCanvas as xt}from"./chartShell.js";import{escHtml as _}from"../../core/utils.js";const h=24,m=5,bt=15,N=110,pt=12;export function renderBarPanel(i,U){const{barColor:P,valueLabel:G}=i;let b=i.title,l=i.data;const{card:r,heading:R,controlsRow:I,body:A}=rt(b,U,i.ownerDocument);if(i.selfControl){const e=i.selfControl;I.appendChild(dt(e.options,e.current,n=>q(n),r.ownerDocument))}const{drill:d}=i;let y=!1,B=null;const L=r.ownerDocument.createElement("div");Object.assign(L.style,{color:it.label,fontSize:lt.body,marginBottom:nt.sm}),r.insertBefore(L,A);function D(){const e=[];i.onOpen&&e.push("click to open this wafer"),d&&!y&&e.push(`click a ${d.groupLabelText} to see it by wafer`);const n=e.join(", or ");let o=n?`${n[0].toUpperCase()}${n.slice(1)}.`:"";const t=i.reference?.(l);t&&(o=o?`${o} Dashed line: ${t.label}.`:`Dashed line: ${t.label}.`),L.textContent=o}D();const w=r.ownerDocument.createElement("div"),v=()=>i.reference?bt:0,E=()=>f*2+v()+Math.min(l.length,pt)*(h+m);Object.assign(w.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${E()}px`,scrollbarGutter:"stable"}),A.appendChild(w);const a=r.ownerDocument.createElement("canvas");a.style.display="block",a.style.cursor="default",w.appendChild(a);const k=at(r);let x=-1,M=Math.max(1,...l.map(e=>e.value));const Y=e=>G?G(e):`${st(e.value)} (${e.percent.toFixed(1)}%)`;function F(e){const n=f+v()+e*(h+m),o=f+N,t=a.clientWidth-o-z-f;return{y:n,barX:o,barMaxWidth:Math.max(10,t)}}function p(){w.style.maxHeight=`${E()}px`,ut(r,A,E());const e=w.clientWidth,n=f*2+v()+l.length*(h+m),o=xt(a,r,e,n);if(!o)return;const{ctx:t}=o;t.font=`${ot(-1)}px system-ui, sans-serif`,t.textBaseline="middle";const s=mt(r);l.forEach((c,C)=>{const{y:u,barX:T,barMaxWidth:W}=F(C);C===x&&(t.fillStyle=s.bgHover,t.fillRect(0,u-m/2,e,h+m)),t.fillStyle=s.text,t.textAlign="right";const H=c.label.length>16?`${c.label.slice(0,15)}\u2026`:c.label;t.fillText(H,f+N-8,u+h/2),t.fillStyle=s.track,t.fillRect(T,u,W,h);const S=Math.max(1,c.value/M*W);t.fillStyle=P?P(c,C):s.text,t.fillRect(T,u,S,h),t.fillStyle=s.text,t.textAlign="right",t.fillText(Y(c),T+W+z,u+h/2)});const g=i.reference?.(l);if(g&&l.length){const{barX:c,barMaxWidth:C}=F(0),u=Math.round(c+Math.max(0,Math.min(1,g.value/M))*C)+.5,T=f+v()-3,W=f+v()+l.length*(h+m)-m,H=(Z,tt,et)=>{t.save(),t.strokeStyle=Z,t.lineWidth=tt,t.setLineDash(et),t.beginPath(),t.moveTo(u,T),t.lineTo(u,W),t.stroke(),t.restore()};H(s.bg,3.5,[]),H(s.text,1.5,[4,3]),t.save(),t.font="600 10px system-ui, sans-serif",t.textBaseline="middle",t.textAlign="left";const S=4,O=t.measureText(g.label).width+S*2,X=13,j=f+v()-X-1;let $=u-O/2;$=Math.max(c,Math.min($,c+C-O)),t.fillStyle=s.bg,t.strokeStyle=s.border,t.lineWidth=1,t.beginPath(),t.rect($,j,O,X),t.fill(),t.stroke(),t.fillStyle=s.text,t.fillText(g.label,$+S,j+X/2+.5),t.restore()}}function q(e){if(!i.selfControl)return;i.selfControl.current=e;const n=i.selfControl.onChange(e);l=n.data,M=Math.max(1,...l.map(o=>o.value)),x=-1,n.title&&(b=n.title,R.textContent=b),D(),p()}function J(e){if(!d)return;const n=d.onOpenGroup(e);l=n.data,b=n.title,R.textContent=b,M=Math.max(1,...l.map(o=>o.value)),x=-1,y||(y=!0,B=ht(()=>K(),r.ownerDocument),I.appendChild(B)),D(),p()}function K(){if(!d)return;const e=d.onBack();l=e.data,b=e.title,R.textContent=b,M=Math.max(1,...l.map(n=>n.value)),x=-1,y=!1,B?.remove(),B=null,D(),p()}function V(e){const n=Math.floor((e-f+m/2)/(h+m));return n>=0&&n<l.length?n:-1}a.addEventListener("mousemove",e=>{const n=a.getBoundingClientRect(),o=V(e.clientY-n.top),t=o>=0&&d&&!y&&l[o].itemCount>1,s=o>=0&&(t||!!i.onOpen&&o>=0&&!t);if(o!==x&&(x=o,a.style.cursor=s?"pointer":"default",p()),o>=0){const g=l[o],c=t?`<br><em>click to see this ${_(d.groupLabelText)} by wafer</em>`:i.onOpen?"<br><em>click to open this wafer</em>":"";k.innerHTML=`<strong>${_(g.label)}</strong><br>${_(Y(g))}${c}`,k.style.display="block",ft(k,r,e.clientX,e.clientY)}else k.style.display="none"}),a.addEventListener("mouseleave",()=>{x!==-1&&(x=-1,p()),k.style.display="none"}),a.addEventListener("click",e=>{const n=a.getBoundingClientRect(),o=V(e.clientY-n.top);if(o===-1)return;const t=l[o];if(d&&!y&&t.itemCount>1){J(t);return}i.onOpen?.(t)});const Q=ct(r,()=>p());return p(),{card:r,destroy:()=>Q.disconnect()}}
1
+ import{SPACE as nt,fontPx as ot,FONT as lt,CLR as it}from"../toolbar.js";import{cardShell as rt,formatValue as st,observeResize as ct,makeTooltip as at,positionChartTooltip as ft,makeBackButton as ut,makeSegmented as dt,fitRowsHeight as ht,setChartGrow as mt,resolveChartCanvasColors as xt,PADDING as u,VALUE_WIDTH as j,WAFER_MENU_HINT as bt,prepareCanvas as pt}from"./chartShell.js";import{escHtml as X}from"../../core/utils.js";const d=24,m=5,gt=15,z=110,yt=12;export function renderBarPanel(l,U){const{barColor:G,valueLabel:I}=l;let g=l.title,i=l.data;const{card:r,heading:E,controlsRow:P,body:S}=rt(g,U,l.ownerDocument);if(mt(r,"rows"),l.selfControl){const e=l.selfControl;P.appendChild(dt(e.options,e.current,n=>q(n),r.ownerDocument))}const{drill:f}=l;let y=!1,R=null;const A=r.ownerDocument.createElement("div");Object.assign(A.style,{color:it.label,fontSize:lt.body,marginBottom:nt.sm}),r.insertBefore(A,S);function B(){const e=[];l.onOpen&&e.push("click to open this wafer"),f&&!y&&e.push(`click a ${f.groupLabelText} to see it by wafer`);const n=e.join(", or ");let o=n?`${n[0].toUpperCase()}${n.slice(1)}.`:"";const t=l.reference?.(i);t&&(o=o?`${o} Dashed line: ${t.label}.`:`Dashed line: ${t.label}.`),A.textContent=o}B();const k=r.ownerDocument.createElement("div"),v=()=>l.reference?gt:0,Y=()=>u*2+v()+Math.min(i.length,yt)*(d+m);Object.assign(k.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${Y()}px`,scrollbarGutter:"stable"}),S.appendChild(k);const s=r.ownerDocument.createElement("canvas");s.style.display="block",s.style.cursor="default",k.appendChild(s);const w=at(r);let x=-1,M=Math.max(1,...i.map(e=>e.value));const N=e=>I?I(e):`${st(e.value)} (${e.percent.toFixed(1)}%)`;function F(e){const n=u+v()+e*(d+m),o=u+z,t=s.clientWidth-o-j-u;return{y:n,barX:o,barMaxWidth:Math.max(10,t)}}function C(){const e=u*2+v()+i.length*(d+m);k.style.maxHeight=`${ht(r,S,Y(),e)}px`;const n=k.clientWidth,o=pt(s,r,n,e);if(!o)return;const{ctx:t}=o;t.font=`${ot(-1)}px system-ui, sans-serif`,t.textBaseline="middle";const c=xt(r);i.forEach((a,p)=>{const{y:h,barX:W,barMaxWidth:H}=F(p);p===x&&(t.fillStyle=c.bgHover,t.fillRect(0,h-m/2,n,d+m)),t.fillStyle=c.text,t.textAlign="right";const T=a.label.length>16?`${a.label.slice(0,15)}\u2026`:a.label;t.fillText(T,u+z-8,h+d/2),t.fillStyle=c.track,t.fillRect(W,h,H,d);const $=Math.max(1,a.value/M*H);t.fillStyle=G?G(a,p):c.text,t.fillRect(W,h,$,d),t.fillStyle=c.text,t.textAlign="right",t.fillText(N(a),W+H+j,h+d/2)});const b=l.reference?.(i);if(b&&i.length){const{barX:a,barMaxWidth:p}=F(0),h=Math.round(a+Math.max(0,Math.min(1,b.value/M))*p)+.5,W=u+v()-3,H=u+v()+i.length*(d+m)-m,T=(Z,tt,et)=>{t.save(),t.strokeStyle=Z,t.lineWidth=tt,t.setLineDash(et),t.beginPath(),t.moveTo(h,W),t.lineTo(h,H),t.stroke(),t.restore()};T(c.bg,3.5,[]),T(c.text,1.5,[4,3]),t.save(),t.font="600 10px system-ui, sans-serif",t.textBaseline="middle",t.textAlign="left";const $=4,O=t.measureText(b.label).width+$*2,_=13,V=u+v()-_-1;let D=h-O/2;D=Math.max(a,Math.min(D,a+p-O)),t.fillStyle=c.bg,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.rect(D,V,O,_),t.fill(),t.stroke(),t.fillStyle=c.text,t.fillText(b.label,D+$,V+_/2+.5),t.restore()}}function q(e){if(!l.selfControl)return;l.selfControl.current=e;const n=l.selfControl.onChange(e);i=n.data,M=Math.max(1,...i.map(o=>o.value)),x=-1,n.title&&(g=n.title,E.textContent=g),B(),C()}function J(e){if(!f)return;const n=f.onOpenGroup(e);i=n.data,g=n.title,E.textContent=g,M=Math.max(1,...i.map(o=>o.value)),x=-1,y||(y=!0,R=ut(()=>K(),r.ownerDocument),P.appendChild(R)),B(),C()}function K(){if(!f)return;const e=f.onBack();i=e.data,g=e.title,E.textContent=g,M=Math.max(1,...i.map(n=>n.value)),x=-1,y=!1,R?.remove(),R=null,B(),C()}function L(e){const n=Math.floor((e-u+m/2)/(d+m));return n>=0&&n<i.length?n:-1}s.addEventListener("mousemove",e=>{const n=s.getBoundingClientRect(),o=L(e.clientY-n.top),t=o>=0&&f&&!y&&i[o].itemCount>1,c=o>=0&&(t||!!l.onOpen&&o>=0&&!t);if(o!==x&&(x=o,s.style.cursor=c?"pointer":"default",C()),o>=0){const b=i[o],a=t?`<br><em>click to see this ${X(f.groupLabelText)} by wafer</em>`:l.onOpen?"<br><em>click to open this wafer</em>":"",p=!t&&l.onWaferContextMenu&&typeof b.key=="number"?bt:"";w.innerHTML=`<strong>${X(b.label)}</strong><br>${X(N(b))}${a}${p}`,w.style.display="block",ft(w,r,e.clientX,e.clientY)}else w.style.display="none"}),s.addEventListener("mouseleave",()=>{x!==-1&&(x=-1,C()),w.style.display="none"}),s.addEventListener("click",e=>{const n=s.getBoundingClientRect(),o=L(e.clientY-n.top);if(o===-1)return;const t=i[o];if(f&&!y&&t.itemCount>1){J(t);return}l.onOpen?.(t)}),s.addEventListener("contextmenu",e=>{const n=s.getBoundingClientRect(),o=L(e.clientY-n.top);if(o===-1||!l.onWaferContextMenu)return;const t=i[o];f&&!y&&t.itemCount>1||typeof t.key!="number"||(w.style.display="none",l.onWaferContextMenu(t.key,void 0,e))});const Q=ct(r,()=>C());return C(),{card:r,destroy:()=>Q.disconnect()}}
@@ -1,6 +1,6 @@
1
1
  import { type BoxplotItem } from '../../stats/boxplot.js';
2
2
  import type { TestDef } from '../../renderer/buildWaferMap.js';
3
- import { type AxisPrefs, type SaveImageHandler } from './chartShell.js';
3
+ import { type AxisPrefs, type SaveImageHandler, type WaferContextMenuHandler } from './chartShell.js';
4
4
  /** A `BoxplotItem` carrying enough identity for the panel to open its wafer
5
5
  * on a leaf-row click — the tab's own item shape already has this. */
6
6
  export type BoxplotPanelItem = BoxplotItem & {
@@ -43,6 +43,9 @@ export interface BoxplotPanelOptions {
43
43
  * same test in value mode instead of defaulting to hard-bin mode.
44
44
  * Never called for a pooled group-overview row (that drills instead). */
45
45
  onOpen?: (waferIndex: number, testNumber: number) => void;
46
+ /** Right-click on a wafer's box — see `WaferContextMenuHandler`. Not called
47
+ * for a pooled group row. */
48
+ onWaferContextMenu?: WaferContextMenuHandler;
46
49
  /** What `onOpen` will actually do, in the user's words — substituted into
47
50
  * both click affordances ("click a box to …" / "click to …"). Default
48
51
  * wording describes opening that wafer, which is what a gallery host does;
@@ -1 +1 @@
1
- import{buildTestBoxplotData as Ze}from"../../stats/boxplot.js";import{SPACE as De,fontPx as N,FONT as et,CLR as tt}from"../toolbar.js";import{fmt as xe}from"../../renderer/fmt.js";import{QUANTITY as Ie}from"./palette.js";import{cardShell as nt,observeResize as ot,makeTooltip as it,positionChartTooltip as lt,makeBackButton as st,makeLinkedTestSelect as at,makeToggle as rt,makeLinkedAxisPrefs as ct,renderEmptyState as He,growCardToFitContent as ft,resolveChartCanvasColors as mt,makeAxisFormat as ut,resolveAxisRange as dt,shouldIncludeLimitsByDefault as xt,drawOffAxisLimits as pt,limitLabelSide as ht,PADDING as L,VALUE_WIDTH as gt,prepareCanvas as bt}from"./chartShell.js";import{escHtml as M}from"../../core/utils.js";const k=24,C=5,Re=110,Tt=12,Oe=20;export function renderBoxplotPanel(h){const{title:Z="Test value distribution",items:We,testDefs:pe,onSaveImage:Ee,groups:u,groupLabelText:ee="group",onOpen:q}=h,Pe=h.openActionLabel??"open that wafer",_e=h.openActionLabel??"open this wafer",{card:a,heading:Ge,body:d,controlsRow:Y}=nt(Z,Ee,h.ownerDocument);a.style.flex="0 0 auto",d.style.flex="0 0 auto",a.style.alignSelf="start";const te=pe.filter(n=>n.testNumber!==void 0);let v=h.selectedTestNumber??te[0]?.testNumber??null,ne=!1,Q=0,x=null,H=null,oe=null,he=null;function Xe(){const n=!u||u.length===0?"":x??"\0overview";if(oe&&he===n)return oe;const m=!u||u.length===0?We:x!==null?u.find(g=>g.key===x)?.items??[]:u.map(g=>({label:g.key,dies:g.items.flatMap(c=>c.dies??[])}));return oe=m,he=n,m}function ge(){x!==null&&!H?(H=st(()=>{be(null),h.onGroupChange?.(null)},a.ownerDocument),Y.appendChild(H)):x===null&&H&&(H.remove(),H=null),Ge.textContent=x!==null?`${Z} \u2014 ${ee}: ${x}`:Z}function be(n){return x===n||n!==null&&!(u??[]).some(m=>m.key===n)?!1:(x=n,ge(),U(),S(),!0)}const Te=at(te,v,n=>{v=n,S(),h.onTestChange?.(n)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Ne=Te.el;Y.appendChild(Ne),Y.appendChild(rt("Log scale",ne,n=>{ne=n,S()},a.ownerDocument));const ie=a.ownerDocument.createElement("span");Object.assign(ie.style,{display:"inline-flex",gap:De.lg,alignItems:"center"}),Y.appendChild(ie);const le=ct(ie,h.axisPrefs,n=>{h.onAxisPrefsChange?.(n),S()},a.ownerDocument),se=a.ownerDocument.createElement("div");Object.assign(se.style,{color:tt.label,fontSize:et.body,marginBottom:De.sm}),a.insertBefore(se,d);function U(){const n=!!u&&u.length>0&&x===null,m=[];n?m.push(`click a ${ee}'s box to see it by wafer`):q&&m.push(`click a box to ${Pe}`);const g=m.length?`${m[0][0].toUpperCase()}${m[0].slice(1)} \xB7 `:"";se.textContent=`${g}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(Q?` \xB7 axis clipped, ${Q} value${Q===1?"":"s"} outside`:"")}U(),ge();const O=it(a);let ae=null;function S(){const{includeLimits:n,clipOutliers:m}=le.get();if(U(),d.innerHTML="",te.length===0||v===null){He(d,"No parametric test data available for box plots.");return}const g=Xe(),c=Ze(g,v),re=pe.find(t=>t.testNumber===v),z=re?.unit,F=re?.limitLow,V=re?.limitHigh;if(c.every(t=>t.count===0)){He(d,"No parametric test data available for box plots.");return}const p=a.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",d.appendChild(p);const ye=L*2+Math.min(c.length,Tt)*(k+C)+Oe;d.style.maxHeight=`${ye}px`,d.style.overflowY="auto",d.style.scrollbarGutter="stable";let W=-1;const ce=c.filter(t=>t.count>0),ve=Math.min(...ce.map(t=>t.min)),we=Math.max(...ce.map(t=>t.max)),Le=n??xt(ve,we,F,V);le.sync(Le,F!==void 0||V!==void 0);const E=dt({dataMin:ve,dataMax:we,limitLow:F,limitHigh:V,includeLimits:Le,clipOutliers:m,values:ce.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});Q=E.clippedCount,U();const R=E.lo,fe=E.hi,Me=fe-R||1,j=ne&&R>0,me=j?Math.log10(R):0,ke=(j?Math.log10(fe):0)-me||1,ue=ut(Math.max(Math.abs(R),Math.abs(fe)),z);function Qe(){const t=L+Re,i=p.clientWidth-t-gt-L;return{plotX:t,plotMaxWidth:Math.max(10,i)}}function A(t,i,s){return j?i+(Math.log10(t)-me)/ke*s:i+(t-R)/Me*s}function Ue(){const t=[];for(let i=0;i<=4;i++)t.push(j?Math.pow(10,me+ke*i/4):R+Me*i/4);return t}function K(){const t=mt(a),i=d.clientWidth,s=L*2+c.length*(k+C)+Oe,$=bt(p,a,i,s);if(!$)return;const{ctx:e}=$;e.font=`${N(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:f}=Qe();c.forEach((o,b)=>{const y=L+b*(k+C),r=y+k/2;b===W&&(e.fillStyle=t.bgHover,e.fillRect(0,y-C/2,i,k+C)),e.fillStyle=t.text,e.textAlign="right";const Fe=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(Fe,L+Re-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const Ve=l+f,_=Je=>Math.min(Ve,Math.max(l,Je)),Ae=A(o.min,l,f),$e=A(o.max,l,f),B=_(Ae),G=_(A(o.q1,l,f)),Be=_(A(o.median,l,f)),de=_(A(o.q3,l,f)),D=_($e),je=Ae<B,Ke=$e>D,I=y+3,X=y+k-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(B,r),e.lineTo(G,r),e.moveTo(de,r),e.lineTo(D,r);const w=3;je?(e.moveTo(B+w,r-w),e.lineTo(B,r),e.lineTo(B+w,r+w)):(e.moveTo(B,I),e.lineTo(B,X)),Ke?(e.moveTo(D-w,r-w),e.lineTo(D,r),e.lineTo(D-w,r+w)):(e.moveTo(D,I),e.lineTo(D,X)),e.stroke(),e.fillStyle=Ie,e.globalAlpha=.35,e.fillRect(G,I,Math.max(1,de-G),X-I),e.globalAlpha=1,e.strokeStyle=Ie,e.strokeRect(G,I,Math.max(1,de-G),X-I),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Be,I),e.lineTo(Be,X),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(xe(o.median,z,"engineering"),l+f+10,r)});const T=L+c.length*(k+C);e.font=`${N(-1)}px system-ui, sans-serif`,pt(e,E.offAxis,{left:l,right:l+f,top:0,bottom:T},"horizontal",t.limitLine,o=>xe(o,z,"engineering"));const ze=new Set(E.offAxis.map(o=>o.value));for(const[o,b]of[[F,"LSL"],[V,"USL"]]){if(o===void 0||ze.has(o))continue;const y=A(o,l,f);e.strokeStyle=t.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(y,0),e.lineTo(y,T),e.stroke(),e.setLineDash([]),e.fillStyle=t.limitLine;const r=ht(y,e.measureText(b).width,l,l+f,b==="LSL");e.textAlign=r<0?"right":"left",e.textBaseline="bottom",e.fillText(b,y+r*3,T-1)}e.font=`${N(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,T),e.lineTo(l+f,T),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";for(const o of Ue()){const b=A(o,l,f);e.beginPath(),e.moveTo(b,T),e.lineTo(b,T+4),e.stroke(),e.fillText(ue.tick(o),b,T+6)}ue.unitLabel&&(e.textAlign="left",e.font=`${N(-1)}px system-ui, sans-serif`,e.fillText(`(${ue.unitLabel})`,l+f+22,T+6),e.font=`${N(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function Ce(t){const i=Math.floor((t-L+C/2)/(k+C));return i>=0&&i<c.length?i:-1}function P(t){return xe(t,z,"engineering")}const J=!!u&&u.length>0&&x===null,Se=t=>!J&&!!q&&g[t]?.waferIndex!==void 0;p.addEventListener("mousemove",t=>{const i=p.getBoundingClientRect(),s=Ce(t.clientY-i.top),$=s>=0&&c[s].count>0&&(J||Se(s));if(s!==W&&(W=s,p.style.cursor=$?"pointer":"default",K()),s>=0&&c[s].count>0){const e=c[s],l=J?`<br><em>click to see this ${M(ee)} by wafer</em>`:Se(s)?`<br><em>click to ${M(_e)}</em>`:"";O.innerHTML=`<strong>${M(e.label)}</strong> (${e.count} dies)<br>max ${M(P(e.max))}<br>q3 ${M(P(e.q3))}<br>median ${M(P(e.median))}<br>q1 ${M(P(e.q1))}<br>min ${M(P(e.min))}${l}`,O.style.display="block",lt(O,a,t.clientX,t.clientY)}else O.style.display="none"}),p.addEventListener("mouseleave",()=>{W!==-1&&(W=-1,K()),O.style.display="none"}),p.addEventListener("click",t=>{const i=p.getBoundingClientRect(),s=Ce(t.clientY-i.top);if(s===-1||c[s].count===0)return;if(J){const e=c[s].label;be(e),h.onGroupChange?.(e);return}const $=g[s]?.waferIndex;q&&$!==void 0&&v!==null&&q($,v)}),ae?.disconnect(),ae=ot(a,()=>K()),K(),ft(a,d,ye)}S();function qe(n){Te.set(n)&&(v=n,S())}function Ye(n){le.set(n)&&S()}return{card:a,setTest:qe,setAxisPrefs:Ye,destroy:()=>ae?.disconnect()}}
1
+ import{buildTestBoxplotData as nt}from"../../stats/boxplot.js";import{SPACE as Be,fontPx as Y,FONT as it,CLR as ot}from"../toolbar.js";import{fmt as he}from"../../renderer/fmt.js";import{fitTicks as lt}from"../../renderer/axisTicks.js";import{QUANTITY as He}from"./palette.js";import{cardShell as st,observeResize as at,makeTooltip as rt,positionChartTooltip as ct,makeBackButton as ft,makeLinkedTestSelect as ut,makeToggle as mt,makeLinkedAxisPrefs as dt,renderEmptyState as Re,fitRowsHeight as xt,setChartGrow as pt,resolveChartCanvasColors as ht,makeAxisFormat as De,horizontalTickSpacing as gt,resolveAxisRange as bt,shouldIncludeLimitsByDefault as Tt,drawOffAxisLimits as yt,limitLabelSide as wt,PADDING as L,VALUE_WIDTH as vt,WAFER_MENU_HINT as kt,prepareCanvas as Lt}from"./chartShell.js";import{escHtml as M,maxOf as Mt,minOf as Ct}from"../../core/utils.js";const C=24,S=5,We=110,St=12,Oe=20;export function renderBoxplotPanel(u){const{title:ee="Test value distribution",items:Ee,testDefs:ge,onSaveImage:Pe,groups:g,groupLabelText:te="group",onOpen:U}=u,_e=u.openActionLabel??"open that wafer",Ge=u.openActionLabel??"open this wafer",{card:a,heading:Ne,body:x,controlsRow:z}=st(ee,Pe,u.ownerDocument);pt(a,"rows"),a.style.flex="0 0 auto",x.style.flex="0 0 auto",a.style.alignSelf="start";const ne=ge.filter(i=>i.testNumber!==void 0);let y=u.selectedTestNumber??ne[0]?.testNumber??null,ie=!1,Q=0,b=null,R=null,oe=null,be=null;function Xe(){const i=!g||g.length===0?"":b??"\0overview";if(oe&&be===i)return oe;const p=!g||g.length===0?Ee:b!==null?g.find(h=>h.key===b)?.items??[]:g.map(h=>({label:h.key,dies:h.items.flatMap(f=>f.dies??[])}));return oe=p,be=i,p}function Te(){b!==null&&!R?(R=ft(()=>{ye(null),u.onGroupChange?.(null)},a.ownerDocument),z.appendChild(R)):b===null&&R&&(R.remove(),R=null),Ne.textContent=b!==null?`${ee} \u2014 ${te}: ${b}`:ee}function ye(i){return b===i||i!==null&&!(g??[]).some(p=>p.key===i)?!1:(b=i,Te(),F(),A(),!0)}const we=ut(ne,y,i=>{y=i,A(),u.onTestChange?.(i)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),qe=we.el;z.appendChild(qe),z.appendChild(mt("Log scale",ie,i=>{ie=i,A()},a.ownerDocument));const le=a.ownerDocument.createElement("span");Object.assign(le.style,{display:"inline-flex",gap:Be.lg,alignItems:"center"}),z.appendChild(le);const se=dt(le,u.axisPrefs,i=>{u.onAxisPrefsChange?.(i),A()},a.ownerDocument),ae=a.ownerDocument.createElement("div");Object.assign(ae.style,{color:ot.label,fontSize:it.body,marginBottom:Be.sm}),a.insertBefore(ae,x);function F(){const i=!!g&&g.length>0&&b===null,p=[];i?p.push(`click a ${te}'s box to see it by wafer`):U&&p.push(`click a box to ${_e}`);const h=p.length?`${p[0][0].toUpperCase()}${p[0].slice(1)} \xB7 `:"";ae.textContent=`${h}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(Q?` \xB7 axis clipped, ${Q} value${Q===1?"":"s"} outside`:"")}F(),Te();const D=rt(a);let re=null;function A(){const{includeLimits:i,clipOutliers:p}=se.get();if(F(),x.innerHTML="",ne.length===0||y===null){Re(x,"No parametric test data available for box plots.");return}const h=Xe(),f=nt(h,y),ce=ge.find(t=>t.testNumber===y),W=ce?.unit,V=ce?.limitLow,j=ce?.limitHigh;if(f.every(t=>t.count===0)){Re(x,"No parametric test data available for box plots.");return}const m=a.ownerDocument.createElement("canvas");m.style.display="block",m.style.cursor="default",x.appendChild(m);const ve=L*2+Math.min(f.length,St)*(C+S)+Oe;x.style.maxHeight=`${ve}px`,x.style.overflowY="auto",x.style.scrollbarGutter="stable";let P=-1;const fe=f.filter(t=>t.count>0),ke=Ct(fe.map(t=>t.min)),Le=Mt(fe.map(t=>t.max)),Me=i??Tt(ke,Le,V,j);se.sync(Me,V!==void 0||j!==void 0);const _=bt({dataMin:ke,dataMax:Le,limitLow:V,limitHigh:j,includeLimits:Me,clipOutliers:p,values:fe.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});Q=_.clippedCount,F();const O=_.lo,K=_.hi,ze=K-O||1,J=ie&&O>0,ue=J?Math.log10(O):0,Ce=(J?Math.log10(K):0)-ue||1,me=Math.max(Math.abs(O),Math.abs(K));function Qe(){const t=L+We,s=m.clientWidth-t-vt-L;return{plotX:t,plotMaxWidth:Math.max(10,s)}}function $(t,s,n){return J?s+(Math.log10(t)-ue)/Ce*n:s+(t-O)/ze*n}function Fe(t,s){if(J){const d=[];for(let e=0;e<=4;e++)d.push(Math.pow(10,ue+Ce*e/4));return{ticks:d,axis:De(me,W)}}const n=lt(O,K,s,gt(t,me,W));return{ticks:n.ticks,axis:De(me,W,n.step||void 0)}}function Z(){const t=ht(a),s=L*2+f.length*(C+S)+Oe;x.style.maxHeight=`${xt(a,x,ve,s)}px`;const n=x.clientWidth,d=Lt(m,a,n,s);if(!d)return;const{ctx:e}=d;e.font=`${Y(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:r}=Qe();f.forEach((o,T)=>{const v=L+T*(C+S),c=v+C/2;T===P&&(e.fillStyle=t.bgHover,e.fillRect(0,v-S/2,n,C+S)),e.fillStyle=t.text,e.textAlign="right";const Ke=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(Ke,L+We-8,c),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,c);return}const Je=l+r,N=tt=>Math.min(Je,Math.max(l,tt)),Ae=$(o.min,l,r),$e=$(o.max,l,r),I=N(Ae),X=N($(o.q1,l,r)),Ie=N($(o.median,l,r)),pe=N($(o.q3,l,r)),B=N($e),Ze=Ae<I,et=$e>B,H=v+3,q=v+C-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(I,c),e.lineTo(X,c),e.moveTo(pe,c),e.lineTo(B,c);const k=3;Ze?(e.moveTo(I+k,c-k),e.lineTo(I,c),e.lineTo(I+k,c+k)):(e.moveTo(I,H),e.lineTo(I,q)),et?(e.moveTo(B-k,c-k),e.lineTo(B,c),e.lineTo(B-k,c+k)):(e.moveTo(B,H),e.lineTo(B,q)),e.stroke(),e.fillStyle=He,e.globalAlpha=.35,e.fillRect(X,H,Math.max(1,pe-X),q-H),e.globalAlpha=1,e.strokeStyle=He,e.strokeRect(X,H,Math.max(1,pe-X),q-H),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Ie,H),e.lineTo(Ie,q),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(he(o.median,W,"engineering"),l+r+10,c)});const w=L+f.length*(C+S);e.font=`${Y(-1)}px system-ui, sans-serif`,yt(e,_.offAxis,{left:l,right:l+r,top:0,bottom:w},"horizontal",t.limitLine,o=>he(o,W,"engineering"));const Ve=new Set(_.offAxis.map(o=>o.value));for(const[o,T]of[[V,"LSL"],[j,"USL"]]){if(o===void 0||Ve.has(o))continue;const v=$(o,l,r);e.strokeStyle=t.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(v,0),e.lineTo(v,w),e.stroke(),e.setLineDash([]),e.fillStyle=t.limitLine;const c=wt(v,e.measureText(T).width,l,l+r,T==="LSL");e.textAlign=c<0?"right":"left",e.textBaseline="bottom",e.fillText(T,v+c*3,w-1)}e.font=`${Y(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,w),e.lineTo(l+r,w),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:je,axis:xe}=Fe(e,r);for(const o of je){const T=$(o,l,r);e.beginPath(),e.moveTo(T,w),e.lineTo(T,w+4),e.stroke(),e.fillText(xe.tick(o),T,w+6)}xe.unitLabel&&(e.textAlign="left",e.font=`${Y(-1)}px system-ui, sans-serif`,e.fillText(`(${xe.unitLabel})`,l+r+22,w+6),e.font=`${Y(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function de(t){const s=Math.floor((t-L+S/2)/(C+S));return s>=0&&s<f.length?s:-1}function G(t){return he(t,W,"engineering")}const E=!!g&&g.length>0&&b===null,Se=t=>!E&&!!U&&h[t]?.waferIndex!==void 0;m.addEventListener("mousemove",t=>{const s=m.getBoundingClientRect(),n=de(t.clientY-s.top),d=n>=0&&f[n].count>0&&(E||Se(n));if(n!==P&&(P=n,m.style.cursor=d?"pointer":"default",Z()),n>=0&&f[n].count>0){const e=f[n],l=E?`<br><em>click to see this ${M(te)} by wafer</em>`:Se(n)?`<br><em>click to ${M(Ge)}</em>`:"",r=!E&&u.onWaferContextMenu&&h[n]?.waferIndex!==void 0?kt:"";D.innerHTML=`<strong>${M(e.label)}</strong> (${e.count} dies)<br>max ${M(G(e.max))}<br>q3 ${M(G(e.q3))}<br>median ${M(G(e.median))}<br>q1 ${M(G(e.q1))}<br>min ${M(G(e.min))}${l}${r}`,D.style.display="block",ct(D,a,t.clientX,t.clientY)}else D.style.display="none"}),m.addEventListener("mouseleave",()=>{P!==-1&&(P=-1,Z()),D.style.display="none"}),m.addEventListener("click",t=>{const s=m.getBoundingClientRect(),n=de(t.clientY-s.top);if(n===-1||f[n].count===0)return;if(E){const e=f[n].label;ye(e),u.onGroupChange?.(e);return}const d=h[n]?.waferIndex;U&&d!==void 0&&y!==null&&U(d,y)}),m.addEventListener("contextmenu",t=>{const s=m.getBoundingClientRect(),n=de(t.clientY-s.top),d=n===-1?void 0:h[n]?.waferIndex;E||d===void 0||!u.onWaferContextMenu||(D.style.display="none",u.onWaferContextMenu(d,y??void 0,t))}),re?.disconnect(),re=at(a,()=>Z()),Z()}A();function Ye(i){we.set(i)&&(y=i,A())}function Ue(i){se.set(i)&&A()}return{card:a,setTest:Ye,setAxisPrefs:Ue,destroy:()=>re?.disconnect()}}
@@ -1 +1 @@
1
- import{buildCapabilityData as ue}from"../../stats/capability.js";import{capabilityColor as B}from"./palette.js";import{LEADING as be,SPACE as g,fontPx as G,FONT as Q,CLR as H}from"../toolbar.js";import{cardShell as ye,chartFillHeight as xe,applyCanvasFlow as ge,observeResize as ve,makeTooltip as Se,positionChartTooltip as Me,renderEmptyState as Te,resolveChartCanvasColors as Le,chartSwatchCss as ke,prepareCanvas as Ce,chartDpr as Pe}from"./chartShell.js";import{fmt as $e}from"../../renderer/fmt.js";import{escHtml as ee}from"../../core/utils.js";const Ae=30,We=160,te=90,ne=28,v=42;function I(C){return C===null?"\u2014":C.toFixed(2)}export function renderCapabilityPanel(C){const{title:oe="Process capability",items:le,testDefs:z,onSaveImage:se,onSelectTest:O}=C;let X=C.selectedTestNumber??null;const{card:r,body:S}=ye(oe,se,C.ownerDocument),ie=z.some(n=>n.testNumber!==void 0);S.style.overflowX="auto";function ae(){return le}const P=r.ownerDocument.createElement("div");Object.assign(P.style,{display:"flex",flexDirection:"column",gap:g.xs,marginBottom:g.sm}),r.insertBefore(P,S);const W=Se(r);let R=()=>{};function re(n,i,f){P.innerHTML="";const c=r.ownerDocument.createElement("span");Object.assign(c.style,{display:"inline-flex",alignItems:"center",gap:g.sm,color:H.value,fontSize:Q.body,fontWeight:"500"});const M=f-n,b=i>0?` \xB7 ${i} without spec limits`:"",y=r.ownerDocument.createElement("span");y.textContent=M>0?`${n} of ${f} tests shown${b} \xB7 ${M} excluded (no recorded values)`:`${n} test${n!==1?"s":""} shown${b}`,c.appendChild(y),P.appendChild(c);const $=i>=n,T=r.ownerDocument.createElement("div");T.textContent=$?"No test has both spec limits, so each is normalised to its own observed range (min = 0, max = 1) and sorted most-variable first. Ppk needs limits, so none is shown.":"Normalised to spec limits (LSL = 0, USL = 1), worst Ppk first. Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(T.style,{color:H.label,fontSize:Q.body,lineHeight:be.base,marginTop:g.xs,maxWidth:"78ch"}),P.appendChild(T),P.appendChild(ce(r.ownerDocument,i>0,$))}function ce(n,i,f=!1){const c=n.createElement("div");Object.assign(c.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${g.xs} ${g.xl}`,marginTop:g.xl,marginBottom:g.xs,fontSize:Q.body,color:H.label});const M=f?[]:[{color:B(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:B(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:B(.5),label:"Poor \xB7 Ppk < 1.0"}];i&&M.push({color:H.label,label:"No spec limits \xB7 no Ppk",dashed:!0});for(const b of M){const y=n.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:g.sm});const $=n.createElement("span");$.style.cssText=ke(b.color,b.dashed?"outline":"solid");const T=n.createElement("span");T.textContent=b.label,y.append($,T),c.appendChild(y)}return c}function me(n){if(S.innerHTML="",n.length===0)return Te(S,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const i=r.ownerDocument.createElement("canvas");i.style.display="block",i.style.cursor="default",S.appendChild(i);const f=n.length,c=Math.min(0,...n.map(t=>t.min)),M=Math.max(1,...n.map(t=>t.max)),b=(M-c)*.05||.1,y=c-b,T=M+b-y||1;function V(t){return Math.max(Ae,Math.min(We,Math.floor(t/f)))}function L(t,s,o){return s+(1-(t-y)/T)*o}function he(t,s,o,l,k){const x=n.filter(a=>a.hasSpec).length,A=x===n.length,d=x===0,_=d?"max":"USL",e=d?"min":"LSL",h=A?"normalised to spec":d?"normalised to range":"normalised (per test)";t.font=`${G(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle";for(const[a,D]of[[1,_],[0,e]]){const p=L(a,o,l);p<o-1||p>o+l+1||(t.strokeStyle=s.limitLine,t.lineWidth=1,t.beginPath(),t.moveTo(v-5,p),t.lineTo(v,p),t.stroke(),t.fillStyle=s.textMuted,t.fillText(D,v-8,p))}t.save(),t.translate(9,o+l/2),t.rotate(-Math.PI/2),t.textAlign="center",t.fillStyle=s.textMuted,t.fillText(h,0,0),t.restore()}let w=-1;function j(){ge(i);const t=S.clientWidth,s=V(t-v),o=s*f,l=ne,k=v+o,x=xe(r,S,i,ne+200+te),A=Math.max(l+60,x-te),d=A-l,_=Ce(i,r,k,x);if(!_)return;const{ctx:e}=_,h=Le(r);n.forEach((a,D)=>{const p=v+D*s,m=p+s/2,u=Math.max(4,s*.55);if(D===w&&(e.fillStyle=h.bgHover,e.fillRect(p,l,s,A-l)),n[D].testNumber===X&&(e.fillStyle=h.accent,e.fillRect(p,A-2,s,2)),a.hasSpec){const K=L(0,l,d),Z=L(1,l,d);e.strokeStyle=h.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(p,K),e.lineTo(p+s,K),e.moveTo(p,Z),e.lineTo(p+s,Z),e.stroke(),e.setLineDash([])}const F=a.hasSpec?B(a.ppk):h.textMuted,U=L(a.min,l,d),E=L(a.q1,l,d),J=L(a.median,l,d),N=L(a.q3,l,d),q=L(a.max,l,d);e.strokeStyle=h.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(m,U),e.lineTo(m,E),e.moveTo(m,N),e.lineTo(m,q),e.moveTo(m-u/4,U),e.lineTo(m+u/4,U),e.moveTo(m-u/4,q),e.lineTo(m+u/4,q),e.stroke(),e.fillStyle=F,e.globalAlpha=.5,e.fillRect(m-u/2,Math.min(E,N),u,Math.max(1,Math.abs(N-E))),e.globalAlpha=1,e.strokeStyle=F,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(m-u/2,Math.min(E,N),u,Math.max(1,Math.abs(N-E))),e.setLineDash([]),e.strokeStyle=h.text,e.lineWidth=2,e.beginPath(),e.moveTo(m-u/2,J),e.lineTo(m+u/2,J),e.stroke(),e.lineWidth=1,e.font=`${G(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?F:h.textMuted,e.fillText(a.hasSpec?I(a.ppk):"\u2014",m,l-8);const fe=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(m,A+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${G(-1)}px system-ui, sans-serif`,e.fillStyle=h.textMuted,e.fillText(fe,0,0),e.restore()}),e.strokeStyle=h.border,e.lineWidth=1,e.strokeRect(v,l,o,d),he(e,h,l,d,o)}function Y(t){const s=i.getBoundingClientRect(),o=S.clientWidth,l=V(o-v),k=(t.clientX-s.left)*(i.width/Pe(i)/s.width)-v,x=Math.floor(k/l);return x>=0&&x<f?x:-1}return i.addEventListener("mousemove",t=>{const s=Y(t);if(s!==w&&(w=s,j()),s===-1){W.style.display="none";return}const o=n[s],l=k=>ee($e(k,o.unit));W.innerHTML=`<strong>${ee(o.label)}</strong> (n=${o.n})<br>`+(o.hasSpec?`LSL ${l(o.lsl)} \xB7 USL ${l(o.usl)}<br>mean ${l(o.mean)}<br>Cp ${I(o.cp)} \xB7 Cpk ${I(o.cpk)}<br>Pp ${I(o.pp)} \xB7 Ppk ${I(o.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${l(o.mean)} \xB7 stddev ${l(o.stdOverall)}`)+(O?"<br><em>click to view in boxplot</em>":""),W.style.display="block",Me(W,r,t.clientX,t.clientY),i.style.cursor=O?"pointer":"default"}),i.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,j()),W.style.display="none"}),i.addEventListener("click",t=>{if(!O)return;const s=Y(t);s!==-1&&O(n[s].testNumber)}),j}function de(){const n=ue(ae(),z),i=z.filter(c=>c.testNumber!==void 0).length,f=n.filter(c=>!c.hasSpec).length;re(n.length,f,i),R=me(n),R()}const pe=ve(r,()=>R());return de(),{card:r,hasData:ie,setTest:n=>{X!==n&&(X=n,R())},destroy:()=>pe.disconnect()}}
1
+ import{buildCapabilityData as xe}from"../../stats/capability.js";import{capabilityColor as j}from"./palette.js";import{LEADING as ge,SPACE as S,fontPx as K,FONT as Q,CLR as R}from"../toolbar.js";import{cardShell as ve,isExpandedCard as Se,chartFillHeight as Me,applyCanvasFlow as Te,observeResize as Ce,makeTooltip as Le,positionChartTooltip as ke,renderEmptyState as $e,resolveChartCanvasColors as Ee,chartSwatchCss as Pe,prepareCanvas as De,chartDpr as Ae}from"./chartShell.js";import{fmt as We}from"../../renderer/fmt.js";import{escHtml as z}from"../../core/utils.js";import{DERIVED_MARK as Y,DERIVED_KEY as oe}from"../../renderer/testLabel.js";const we=30,le=160,se=90,ie=28,T=42;function O(E){return E===null?"\u2014":E.toFixed(2)}export function renderCapabilityPanel(E){const{title:ae="Process capability",items:re,testDefs:X,onSaveImage:ce,onSelectTest:_}=E;let F=E.selectedTestNumber??null;const{card:r,body:C}=ve(ae,ce,E.ownerDocument),me=X.some(n=>n.testNumber!==void 0);C.style.overflowX="auto";function de(){return re}const P=r.ownerDocument.createElement("div");Object.assign(P.style,{display:"flex",flexDirection:"column",gap:S.xs,marginBottom:S.sm}),r.insertBefore(P,C);const A=Le(r);let B=()=>{};function pe(n,i,x,h){P.innerHTML="";const b=r.ownerDocument.createElement("span");Object.assign(b.style,{display:"inline-flex",alignItems:"center",gap:S.sm,color:R.value,fontSize:Q.body,fontWeight:"500"});const L=x-n,m=i>0?` \xB7 ${i} without spec limits`:"",u=r.ownerDocument.createElement("span");u.textContent=L>0?`${n} of ${x} tests shown${m} \xB7 ${L} excluded (no recorded values)`:`${n} test${n!==1?"s":""} shown${m}`,b.appendChild(u),P.appendChild(b);const g=i>=n,M=r.ownerDocument.createElement("div");M.textContent=g?"No test has both spec limits, so each is normalised to its own observed range (min = 0, max = 1) and sorted most-variable first. Ppk needs limits, so none is shown.":"Normalised to spec limits (LSL = 0, USL = 1), worst Ppk first. Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(M.style,{color:R.label,fontSize:Q.body,lineHeight:ge.base,marginTop:S.xs,maxWidth:"78ch"}),P.appendChild(M),P.appendChild(fe(r.ownerDocument,i>0,g,h>0))}function fe(n,i,x=!1,h=!1){const b=n.createElement("div");Object.assign(b.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${S.xs} ${S.xl}`,marginTop:S.xl,marginBottom:S.xs,fontSize:Q.body,color:R.label});const L=x?[]:[{color:j(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:j(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:j(.5),label:"Poor \xB7 Ppk < 1.0"}];i&&L.push({color:R.label,label:"No spec limits \xB7 no Ppk",dashed:!0});for(const m of L){const u=n.createElement("span");Object.assign(u.style,{display:"inline-flex",alignItems:"center",gap:S.sm});const g=n.createElement("span");g.style.cssText=Pe(m.color,m.dashed?"outline":"solid");const M=n.createElement("span");M.textContent=m.label,u.append(g,M),b.appendChild(u)}if(h){const m=n.createElement("span");Object.assign(m.style,{display:"inline-flex",alignItems:"center",gap:S.sm});const u=n.createElement("span");u.textContent=Y,Object.assign(u.style,{color:R.value,fontWeight:"600"});const g=n.createElement("span");g.textContent=oe,m.append(u,g),b.appendChild(m)}return b}function he(n){if(C.innerHTML="",n.length===0)return $e(C,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const i=r.ownerDocument.createElement("canvas");i.style.display="block",i.style.cursor="default",C.appendChild(i);const x=n.length,h=Math.min(0,...n.map(t=>t.min)),b=Math.max(1,...n.map(t=>t.max)),L=(b-h)*.05||.1,m=h-L,g=b+L-m||1;function M(t){const l=Math.floor(t/x);return Math.max(we,Se(r)?l:Math.min(le,l))}function k(t,l,o){return l+(1-(t-m)/g)*o}function ye(t,l,o,s,$){const y=n.filter(a=>a.hasSpec).length,D=y===n.length,d=y===0,H=d?"max":"USL",e=d?"min":"LSL",p=D?"normalised to spec":d?"normalised to range":"normalised (per test)";t.font=`${K(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle";for(const[a,w]of[[1,H],[0,e]]){const f=k(a,o,s);f<o-1||f>o+s+1||(t.strokeStyle=l.limitLine,t.lineWidth=1,t.beginPath(),t.moveTo(T-5,f),t.lineTo(T,f),t.stroke(),t.fillStyle=l.textMuted,t.fillText(w,T-8,f))}t.save(),t.translate(9,o+s/2),t.rotate(-Math.PI/2),t.textAlign="center",t.fillStyle=l.textMuted,t.fillText(p,0,0),t.restore()}let W=-1;function U(){Te(i);const t=C.clientWidth,l=M(t-T),o=l*x,s=ie,$=T+o,y=Me(r,C,i,ie+200+se),D=Math.max(s+60,y-se),d=D-s,H=De(i,r,$,y);if(!H)return;const{ctx:e}=H,p=Ee(r);n.forEach((a,w)=>{const f=T+w*l,c=f+l/2,v=Math.max(4,Math.min(l,le)*.55);if(w===W&&(e.fillStyle=p.bgHover,e.fillRect(f,s,l,D-s)),n[w].testNumber===F&&(e.fillStyle=p.accent,e.fillRect(f,D-2,l,2)),a.hasSpec){const te=k(0,s,d),ne=k(1,s,d);e.strokeStyle=p.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(f,te),e.lineTo(f+l,te),e.moveTo(f,ne),e.lineTo(f+l,ne),e.stroke(),e.setLineDash([])}const V=a.hasSpec?j(a.ppk):p.textMuted,q=k(a.min,s,d),N=k(a.q1,s,d),Z=k(a.median,s,d),I=k(a.q3,s,d),G=k(a.max,s,d);e.strokeStyle=p.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(c,q),e.lineTo(c,N),e.moveTo(c,I),e.lineTo(c,G),e.moveTo(c-v/4,q),e.lineTo(c+v/4,q),e.moveTo(c-v/4,G),e.lineTo(c+v/4,G),e.stroke(),e.fillStyle=V,e.globalAlpha=.5,e.fillRect(c-v/2,Math.min(N,I),v,Math.max(1,Math.abs(I-N))),e.globalAlpha=1,e.strokeStyle=V,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(c-v/2,Math.min(N,I),v,Math.max(1,Math.abs(I-N))),e.setLineDash([]),e.strokeStyle=p.text,e.lineWidth=2,e.beginPath(),e.moveTo(c-v/2,Z),e.lineTo(c+v/2,Z),e.stroke(),e.lineWidth=1,e.font=`${K(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?V:p.textMuted,e.fillText(a.hasSpec?O(a.ppk):"\u2014",c,s-8);const ee=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(c,D+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${K(-1)}px system-ui, sans-serif`,e.fillStyle=p.textMuted,e.fillText(ee,0,0),a.derived&&(e.fillStyle=p.text,e.fillText(`${Y} `,-e.measureText(ee).width,0)),e.restore()}),e.strokeStyle=p.border,e.lineWidth=1,e.strokeRect(T,s,o,d),ye(e,p,s,d,o)}function J(t){const l=i.getBoundingClientRect(),o=C.clientWidth,s=M(o-T),$=(t.clientX-l.left)*(i.width/Ae(i)/l.width)-T,y=Math.floor($/s);return y>=0&&y<x?y:-1}return i.addEventListener("mousemove",t=>{const l=J(t);if(l!==W&&(W=l,U()),l===-1){A.style.display="none";return}const o=n[l],s=y=>z(We(y,o.unit)),$=o.derived?`<em>${Y} ${z(oe)}</em>`+(o.expression?`<br><code>${z(o.expression)}</code>`:"")+"<br>":"";A.innerHTML=`<strong>${z(o.label)}</strong> (n=${o.n})<br>${$}`+(o.hasSpec?`LSL ${s(o.lsl)} \xB7 USL ${s(o.usl)}<br>mean ${s(o.mean)}<br>Cp ${O(o.cp)} \xB7 Cpk ${O(o.cpk)}<br>Pp ${O(o.pp)} \xB7 Ppk ${O(o.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${s(o.mean)} \xB7 stddev ${s(o.stdOverall)}`)+(_?"<br><em>click to view in boxplot</em>":""),A.style.display="block",ke(A,r,t.clientX,t.clientY),i.style.cursor=_?"pointer":"default"}),i.addEventListener("mouseleave",()=>{W!==-1&&(W=-1,U()),A.style.display="none"}),i.addEventListener("click",t=>{if(!_)return;const l=J(t);l!==-1&&_(n[l].testNumber)}),U}function be(){const n=xe(de(),X),i=X.filter(h=>h.testNumber!==void 0).length,x=n.filter(h=>!h.hasSpec).length;pe(n.length,x,i,n.filter(h=>h.derived).length),B=he(n),B()}const ue=Ce(r,()=>B());return be(),{card:r,hasData:me,setTest:n=>{F!==n&&(F=n,B())},destroy:()=>ue.disconnect()}}
@@ -1,13 +1,24 @@
1
1
  import { type SaveImageHandler } from '../toolbar.js';
2
2
  export type { SaveImageHandler };
3
3
  /**
4
- * `triggerBtn`, when given, is hidden for the modal's duration and shown
4
+ * The card's own expand button is hidden for the modal's duration and shown
5
5
  * again on close — a card already sitting inside its own expand modal must
6
6
  * not offer a second "expand" of itself: clicking it would reparent the same
7
7
  * card into a *second*, nested modal, leaving the first one open but empty
8
8
  * (found via exactly that — every card kept its expand icon once expanded).
9
+ * The button is found on the card (`data-wmap-chart-expand`) rather than
10
+ * passed in, so a card opened straight into a modal by something other than
11
+ * its own button — a drilldown — gets the same treatment.
12
+ *
13
+ * `opts.anchor` is for a card that is not on the page yet (see
14
+ * `ReparentModalOptions.anchor`); `opts.onClosed` runs once the modal has
15
+ * closed and the card is back where it came from — the place for a card that
16
+ * existed only for the modal to be destroyed.
9
17
  */
10
- export declare function openChartExpandModal(card: HTMLElement, title: string, triggerBtn?: HTMLButtonElement): void;
18
+ export declare function openChartExpandModal(card: HTMLElement, title: string, opts?: {
19
+ anchor?: Element;
20
+ onClosed?: () => void;
21
+ }): void;
11
22
  export interface ChartCanvasColors {
12
23
  text: string;
13
24
  textMuted: string;
@@ -58,10 +69,10 @@ export declare function observeResize(el: HTMLElement, onResize: () => void): {
58
69
  * (drag-resize, maximize/restore); a "only grow, never shrink" policy would
59
70
  * leave a stale, too-large min-height stuck from a bigger prior size after
60
71
  * the modal shrinks, forcing the exact overflow this function exists to
61
- * prevent. Recomputing from `card`'s current live overhead every call is
62
- * self-correcting in both directions and converges in one extra resize-
63
- * observer pass (chrome overhead doesn't change with `card`'s own height,
64
- * so the fixed point is reached immediately, not oscillating).
72
+ * prevent. The overhead is measured from `card`'s other children
73
+ * (`cardOverhead`), so it is the same whether or not the card is currently
74
+ * stretched, and one call is exact. Ask for the height the content NEEDS,
75
+ * never the height the card was given — the latter can only ever grow.
65
76
  *
66
77
  * The Analysis tab composes several of these cards into one CSS Grid row
67
78
  * per section (`makeChartGridWrap`); nothing about that grid's own row
@@ -77,6 +88,46 @@ export declare function observeResize(el: HTMLElement, onResize: () => void): {
77
88
  * directly with their visible-rows height.
78
89
  */
79
90
  export declare function growCardToFitContent(card: HTMLElement, body: HTMLElement, contentHeight: number): void;
91
+ /**
92
+ * Right-click on one wafer's bar, box or point. The chart hands over the wafer
93
+ * and the test it is showing; the handler owns the event (`preventDefault` to
94
+ * replace the browser's menu) and decides what to offer — Insights opens the
95
+ * drilldown menu on that wafer. Group rows (several wafers pooled) never call it.
96
+ */
97
+ export type WaferContextMenuHandler = (waferIndex: number, testNumber: number | undefined, e: MouseEvent) => void;
98
+ /** The tooltip line saying a mark has one — one wording on every chart. */
99
+ export declare const WAFER_MENU_HINT = "<br><em>right-click to chart this wafer</em>";
100
+ /**
101
+ * How many dies a chart is drawn from, and who they are: "12 dies", "12 dies
102
+ * selected on W03", or "10 of 12 dies selected on W03 (partial and
103
+ * edge-excluded dies left out)" when eligibility dropped some — which a
104
+ * hand-picked selection makes visible in a way a whole wafer does not.
105
+ *
106
+ * `counted` is what the chart actually used; `total` what it was given.
107
+ * `population` omitted = the view's own population, which says so elsewhere.
108
+ */
109
+ export declare function populationPhrase(counted: number, total: number, population?: string): string;
110
+ export type ChartGrow = 'plot' | 'rows' | 'square';
111
+ /** Declare how a card grows when expanded — see the block comment above. */
112
+ export declare function setChartGrow(card: HTMLElement, grow: ChartGrow): void;
113
+ /** True while `card` is in its expand modal. */
114
+ export declare function isExpandedCard(card: HTMLElement): boolean;
115
+ /** Height `body` has for content when it takes all of `card`'s free space,
116
+ * less `body`'s children other than `except`. */
117
+ export declare function bodyRoom(card: HTMLElement, body: HTMLElement, except?: Element): number;
118
+ /**
119
+ * Height to show a list of rows at. The card asks for `cappedHeight` (the grid
120
+ * size, rows beyond it scroll); expanded, it shows as many more rows as the
121
+ * modal has room for, up to `fullHeight`. `besideHeight` is whatever else is in
122
+ * `body` above the rows (a legend).
123
+ *
124
+ * The request never includes the extra: a card that asked for the height it had
125
+ * been given would never shrink when the modal is made smaller.
126
+ *
127
+ * Also records the card's full height, for `openChartExpandModal` to open the
128
+ * box at a size that shows every row.
129
+ */
130
+ export declare function fitRowsHeight(card: HTMLElement, body: HTMLElement, cappedHeight: number, fullHeight: number, besideHeight?: number): number;
80
131
  /**
81
132
  * Canvas height for a "fill the body" chart: siblings already in `body`
82
133
  * above the canvas (a stats label, a legend, ...) get their own natural
@@ -149,11 +200,25 @@ export declare function formatValue(v: number): string;
149
200
  * colorbar uses, so axes never show raw exponent soup like "861E-6" next
150
201
  * to a tooltip that says "861 µA". Falls back to `formatValue` ticks and
151
202
  * no label when the test has no unit.
203
+ *
204
+ * Pass `step` — the tick spacing from `fitTicks` (`renderer/axisTicks.ts`) —
205
+ * and every label carries exactly the decimals that step needs. `tickWithUnit`
206
+ * is the same label with the scaled unit appended, for an axis with no
207
+ * separate unit label.
152
208
  */
153
- export declare function makeAxisFormat(vRef: number, unit: string | undefined): {
209
+ export declare function makeAxisFormat(vRef: number, unit: string | undefined, step?: number): {
154
210
  tick: (v: number) => string;
211
+ tickWithUnit: (v: number) => string;
155
212
  unitLabel: string;
156
213
  };
214
+ /** Minimum distance between tick centres on a vertical axis, px — a label line and room around it. */
215
+ export declare const VERTICAL_TICK_SPACING_PX = 28;
216
+ /**
217
+ * The spacing a horizontal axis's ticks need at a given step: the widest label
218
+ * that step would draw — measured, in the context's current font — plus a gap.
219
+ * Passed to `fitTicks`, which then picks the finest round step whose labels fit.
220
+ */
221
+ export declare function horizontalTickSpacing(ctx: CanvasRenderingContext2D, vRef: number, unit: string | undefined): (step: number, ticks: number[]) => number;
157
222
  /** Draw a small "(unit)" label at (x, y), restoring the context's text state afterward. */
158
223
  export declare function drawAxisUnit(ctx: CanvasRenderingContext2D, unit: string, x: number, y: number, color: string): void;
159
224
  export declare function saveCanvasPng(canvas: HTMLCanvasElement, filenameStem: string, onSaveImage?: SaveImageHandler): void;
@@ -250,7 +315,16 @@ export declare function makeListSelect(options: readonly ListSelectOption[], sel
250
315
  export interface TestSelectItem {
251
316
  testNumber: number;
252
317
  name?: string;
318
+ /** A derived test — listed as `"† name"`, like everywhere else it is named. */
319
+ derived?: true;
253
320
  }
321
+ /**
322
+ * Picker labels for a list of tests: the derived mark in front of a derived
323
+ * test's name, and — when the list holds any — measured names padded by the
324
+ * mark's width so every name starts at about the same x. A list with no derived
325
+ * test is plain names, unchanged.
326
+ */
327
+ export declare function testOptionLabels(items: readonly TestSelectItem[]): string[];
254
328
  /**
255
329
  * Below `MENU_SEARCH_THRESHOLD` tests this is a plain native `<select>`. Past
256
330
  * it, a long native dropdown becomes hard to scan, so it's replaced by a
@@ -1 +1 @@
1
- import{SHADOW as I,LEADING as F,wireControlHover as L,controlStyle as T,SPACE as m,RADIUS as k,fontPx as j,FONT as v,CLR as f,Z_BASE as q,menuLayerFor as G,wireListNavigation as U,MENU_SEARCH_THRESHOLD as D,makeMenuSearchBox as Y,markMenuTrigger as R,saveImageBlob as X,openReparentedModal as Z}from"../toolbar.js";import{ICONS as N}from"../icons.js";import{fmt as K,fmtColorbarAxis as J}from"../../renderer/fmt.js";export function openChartExpandModal(e,r,n){for(const i of e.querySelectorAll('div[style*="position: absolute"]'))i.style.display==="block"&&i.style.pointerEvents==="none"&&(i.style.display="none");const t=e.getAttribute("style")??"";if(Object.assign(e.style,{flex:"1",minHeight:"0",border:"none",borderRadius:"0"}),!Z([e],{title:r,onClosed:()=>{e.setAttribute("style",t),n&&(n.style.display="flex")}})){e.setAttribute("style",t);return}n&&(n.style.display="none")}const Q={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040",accent:"#1a66cc",limitLine:"#b45309"},P={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border",accent:"icon-active",limitLine:"warn-text"};export function resolveChartCanvasColors(e){const r=getComputedStyle(e),n={};for(const t of Object.keys(P)){const o=r.getPropertyValue(`--wmap-${P[t]}`).trim();n[t]=o||Q[t]}return n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,r){const n=e.ownerDocument.defaultView??window;let t=0;const o=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,r()}))});return o.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,o.disconnect()}}}export function growCardToFitContent(e,r,n){const o=`${e.offsetHeight-r.clientHeight+n}px`;e.style.minHeight!==o&&(e.style.minHeight=o)}export function chartFillHeight(e,r,n,t){const o=e.ownerDocument.defaultView??window;let i=0;for(const a of Array.from(r.children)){if(a===n)continue;const s=a,l=o.getComputedStyle(s);i+=s.offsetHeight+(parseFloat(l.marginTop)||0)+(parseFloat(l.marginBottom)||0)}return growCardToFitContent(e,r,i+t),Math.max(t,r.clientHeight-i)}export function chartDpr(e){return(e.ownerDocument.defaultView??window).devicePixelRatio||1}export function prepareCanvas(e,r,n,t){const o=chartDpr(e);e.width=Math.max(1,Math.floor(n*o)),e.height=Math.max(1,Math.floor(t*o)),e.style.width=`${n}px`,e.style.height=`${t}px`;const i=e.getContext("2d");return i?(i.setTransform(o,0,0,o,0,0),i.clearRect(0,0,n,t),i.font=`${j(-1)}px system-ui, sans-serif`,i.textBaseline="middle",{ctx:i,theme:resolveChartCanvasColors(r)}):null}export function applyCanvasFlow(e,r=0){let n;if(typeof r=="number")n=r;else{const t=(r.ownerDocument.defaultView??window).getComputedStyle(r);n=r.offsetTop+r.offsetHeight+(parseFloat(t.marginBottom)||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}`:K(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,r){if(!r)return{tick:formatValue,unitLabel:""};const{tickFmt:n,axisLabel:t}=J(e,null,r);return{tick:n,unitLabel:t}}export function drawAxisUnit(e,r,n,t,o){const i={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font=`${j(-1)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="middle",e.fillStyle=o,e.fillText(`(${r})`,n,t),Object.assign(e,i)}export function saveCanvasPng(e,r,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const o=t.getContext("2d");o.fillStyle="#ffffff",o.fillRect(0,0,t.width,t.height),o.drawImage(e,0,0),t.toBlob(i=>{i&&X(i,r,n)},"image/png")}const z="10px";export function chartSwatchCss(e,r="solid"){return["display:inline-block",`width:${z}`,`height:${z}`,`border-radius:${k.pill}`,`background:${r==="outline"?"transparent":e}`,`border:1px ${r==="outline"?"dashed":"solid"} ${e}`,"box-sizing:border-box","flex-shrink:0"].join(";")}const ee="0.35";export function makeSeriesLegendItem(e,r,n=document){const t=n.createElement("button");t.type="button",Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:m.xs,padding:`${m.xs} ${m.md}`,borderRadius:k.pill,border:`1px solid ${f.menuBorder}`,background:"none",cursor:"pointer",fontSize:v.body,color:f.text,whiteSpace:"nowrap"}),L(t,"bare");const o=n.createElement("span");return o.style.cssText=chartSwatchCss(r),t.append(o,n.createTextNode(e)),{el:t,setState({selected:i,dimmed:a}){t.style.borderColor=i?f.iconActive:f.menuBorder,t.style.fontWeight=i?"600":"400",t.style.opacity=a?ee:"1",t.setAttribute("aria-pressed",i?"true":"false")}}}export function cardFrameStyle(){return{background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:k.container,padding:m.xl,fontVariantNumeric:"tabular-nums"}}export function cardShell(e,r,n=document){const t=n.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",...cardFrameStyle(),minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const o=t.ownerDocument.createElement("div");Object.assign(o.style,{display:"flex",alignItems:"center",gap:m.sm,paddingBottom:"6px",marginBottom:m.md,borderBottom:`1px solid ${f.menuBorder}`});const i=t.ownerDocument.createElement("div");i.textContent=e,Object.assign(i.style,{color:f.value,fontSize:v.heading,fontWeight:"600",flex:"1"}),o.appendChild(i);const a=makeTooltip(t),s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Save as PNG"),s.innerHTML=N.download,Object.assign(s.style,{...T("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:F.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(s),attachChartTip(s,t,a,"Save as PNG"),s.addEventListener("click",()=>{const p=t.querySelector("canvas");p&&saveCanvasPng(p,e,r)}),o.appendChild(s);const l=t.ownerDocument.createElement("button");l.type="button",l.setAttribute("aria-label","Expand"),l.innerHTML=N.expand,Object.assign(l.style,{...T("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:F.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(l),attachChartTip(l,t,a,"Expand"),l.addEventListener("click",()=>openChartExpandModal(t,i.textContent??e,l)),o.appendChild(l),t.appendChild(o);const c=t.ownerDocument.createElement("div");Object.assign(c.style,{display:"flex",gap:m.sm,marginBottom:m.md,flexWrap:"wrap",alignItems:"center"}),t.appendChild(c);const d=t.ownerDocument.createElement("div");return Object.assign(d.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(d),{card:t,heading:i,controlsRow:c,body:d}}export function makeSegmented(e,r,n,t=document,o=!1){const i=t.createElement("div");i.setAttribute("role","radiogroup"),Object.assign(i.style,{display:"inline-flex",border:`1px solid ${f.menuBorder}`,borderRadius:k.control,overflow:"hidden"});const a=`seg-${Math.random().toString(36).slice(2,9)}`,s=[];return e.forEach(([l,c],d)=>{const p=t.createElement("label");Object.assign(p.style,{display:"inline-flex",alignItems:"center",fontSize:v.body,padding:o?"1px 6px":"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:d>0?`1px solid ${f.menuBorder}`:"none"}),L(p,"bare");const u=t.createElement("input");u.type="radio",u.name=a,u.value=l,u.checked=l===r,Object.assign(u.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const w=()=>{p.style.background=u.checked?f.bgActive:f.menuBg,p.style.color=u.checked?f.iconActive:f.text,p.dataset.on=u.checked?"true":"false"};s.push(w),w(),u.addEventListener("change",()=>{if(u.checked){for(const C of s)C();n(l)}}),p.append(u,t.createTextNode(c)),i.appendChild(p)}),i}export function makeBackButton(e,r=document){const n=r.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{...T("outlined"),background:f.menuBg,color:f.label}),L(n),n.addEventListener("click",e),n}export function renderEmptyState(e,r,n){const t=e.ownerDocument.createElement("div");t.textContent=r,Object.assign(t.style,{color:f.label,fontSize:v.body,padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeListSelect(e,r,n,t={}){const{maxWidth:o="200px",ownerDocument:i=document,ariaLabel:a,emptyText:s,searchPlaceholder:l="Filter\u2026",hook:c}=t;let d=r;const p=x=>e.find(O=>O.value===x)?.label??"",u=i.createElement("button");u.type="button",c&&(u.dataset.wmapSelect=c),a&&u.setAttribute("aria-label",a),Object.assign(u.style,{fontSize:v.body,padding:`${m.xxs} ${m.sm}`,background:f.menuBg,color:f.text,border:`1px solid ${f.menuBorder}`,borderRadius:k.control,maxWidth:o,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:m.sm,justifyContent:"space-between"}),L(u),R(u,!1);const w=i.createElement("span");Object.assign(w.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const C=i.createElement("span");C.textContent="\u25BE",C.style.flex="0 0 auto",u.append(w,C);const S=()=>{w.textContent=e.length===0?s??"(none)":p(d)||s||"(none)"};if(S(),e.length===0)return u.disabled=!0,u.style.opacity="0.6",u.style.cursor="default",Object.defineProperty(u,"value",{get:()=>d,set:x=>{d=x,S()}});let h=null;const E=()=>{h&&(h.remove(),h=null,R(u,!1))},A=()=>{E(),u.focus()};function W(){const x=u.getBoundingClientRect(),O=i.defaultView??window,H=Math.max(x.width,220),_=Math.min(x.left,Math.max(4,(O.innerWidth??1/0)-H-4));h=i.createElement("div"),h.setAttribute("role","listbox"),h.setAttribute("aria-label",a??"Options"),Object.assign(h.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${_}px`,background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:k.control,boxShadow:I.menu,zIndex:q,minWidth:`${H}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const $=[],B=()=>$.filter(b=>b.row.style.display!=="none").map(b=>b.row);if(e.length>D){const b=Y(y=>{for(const g of $)g.row.style.display=g.label.includes(y)?"":"none"},l,i);Object.assign(b.style,{position:"sticky",top:"0",zIndex:"1",background:f.menuBg}),b.addEventListener("keydown",y=>{if(y.key==="Escape"){A();return}y.key==="ArrowDown"&&(y.preventDefault(),B()[0]?.focus())}),h.appendChild(b)}for(const b of e){const y=b.value===d,g=i.createElement("div");g.textContent=b.label,g.setAttribute("role","option"),g.setAttribute("aria-selected",y?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:`${m.sm} ${m.xl}`,fontSize:v.body,cursor:"pointer",color:y?f.iconActive:f.text,fontWeight:y?"700":"400",background:y?f.menuActive:"transparent",whiteSpace:"nowrap"}),g.addEventListener("mouseenter",()=>{y||(g.style.background=f.menuHover)}),g.addEventListener("mouseleave",()=>{y||(g.style.background="transparent")}),g.addEventListener("click",V=>{V.stopPropagation(),d=b.value,S(),A(),n(b.value)}),h.appendChild(g),$.push({row:g,label:b.label.toLowerCase()})}if(U(h,B,A),G(u).appendChild(h),R(u,!0),e.length<=D){const b=e.findIndex(y=>y.value===d);B()[b>=0?b:0]?.focus()}}u.addEventListener("click",x=>{if(x.stopPropagation(),h){E();return}W()});const M=x=>{if(!u.isConnected){i.removeEventListener("click",M);return}h&&!h.contains(x.target)&&!u.contains(x.target)&&E()};return i.addEventListener("click",M),Object.defineProperty(u,"value",{get:()=>d,set:x=>{d=x,S()}})}export function makeTestSelect(e,r,n,t={}){const{maxWidth:o="200px",emptyText:i="No parametric tests",ownerDocument:a=document}=t;return makeListSelect(e.map(s=>({value:String(s.testNumber),label:s.name||`Test ${s.testNumber}`})),r!==null?String(r):"",s=>n(Number(s)),{maxWidth:o,ownerDocument:a,ariaLabel:"Test",emptyText:i,searchPlaceholder:"Filter tests\u2026"})}export function makeLinkedAxisPrefs(e,r,n,t=document){let o=r?.includeLimits,i=r?.clipOutliers??!1;const a=()=>n({includeLimits:o,clipOutliers:i});return{get:()=>({includeLimits:o,clipOutliers:i}),sync(s,l){e.innerHTML="",l&&e.appendChild(makeToggle("Axis includes limits",s,c=>{o=c,a()},t)),e.appendChild(makeToggle("Clip outliers",i,c=>{i=c,a()},t))},set(s){return s.includeLimits===o&&s.clipOutliers===i?!1:(o=s.includeLimits,i=s.clipOutliers,!0)}}}export function makeLinkedTestSelect(e,r,n,t={}){let o=r;const i=makeTestSelect(e,o,a=>{a!==o&&(o=a,n(a))},t);return{el:i,get:()=>o,set(a){return o===a||!e.some(s=>s.testNumber===a)?!1:(o=a,i.value=String(a),!0)}}}export const ALL_GROUPS="\0all";export function makeLinkedGroupSelect(e,r,n,t,o={}){let i=n;const a=d=>d??ALL_GROUPS,s=d=>d===ALL_GROUPS?null:d,l=makeLabeledSelect(r,[{value:ALL_GROUPS,label:"All groups"},...e.map(d=>({value:d,label:d}))],a(i),d=>{const p=s(d);p!==i&&(i=p,t(p))},{...o,hook:"group-scope"}),c=l.querySelector('[data-wmap-select="group-scope"]');return{el:l,get:()=>i,set(d){return i===d||d!==null&&!e.includes(d)?!1:(i=d,c&&(c.value=a(d)),!0)}}}export function makeWaferSelect(e,r,n,t={}){const{maxWidth:o="160px",allLabel:i="All wafers",ownerDocument:a=document}=t,s="\0all";return makeListSelect([{value:s,label:i},...e.map((l,c)=>({value:String(c),label:l.label??`#${c}`}))],r===null?s:String(r),l=>n(l===s?null:Number(l)),{maxWidth:o,ownerDocument:a,ariaLabel:i,searchPlaceholder:"Filter wafers\u2026"})}export function makeToggle(e,r,n,t=document){const o=t.createElement("label");Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:m.xs,fontSize:v.body,color:f.label,cursor:"pointer",userSelect:"none"}),L(o,"bare");const i=t.createElement("input");return i.type="checkbox",i.checked=r,i.style.cssText="margin:0;cursor:pointer;",i.addEventListener("change",()=>n(i.checked)),o.append(i,t.createTextNode(e)),o}export function makeLabeledSelect(e,r,n,t,o={}){const{maxWidth:i="160px",hook:a,ownerDocument:s=document}=o,l=s.createElement("label");l.textContent=e,Object.assign(l.style,{color:f.label,fontSize:v.body,display:"flex",alignItems:"center",gap:m.xs});const c=makeListSelect(r,n,t,{maxWidth:i,ownerDocument:s,hook:a,ariaLabel:e.replace(/:\s*$/,"")});return l.appendChild(c),l}export function makeChartGridWrap(e=document){const r=e.createElement("div");return r.dataset.wmapChartGrid="1",Object.assign(r.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:m.lg,flex:"0 0 auto"}),r}export function makeTooltip(e){const r=e.ownerDocument.createElement("div");return Object.assign(r.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:"rgba(30, 32, 40, 0.93)",color:"#f0f0f2",border:"1px solid rgba(255,255,255,0.10)",borderRadius:k.control,padding:`${m.sm} ${m.xl}`,fontSize:v.sub,fontFamily:v.family,maxWidth:"280px",whiteSpace:"nowrap",boxShadow:I.menu}),e.appendChild(r),r}export function attachChartTip(e,r,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",o=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,r,o.clientX,o.clientY)}),e.addEventListener("mousemove",o=>{n.style.display==="block"&&positionChartTooltip(n,r,o.clientX,o.clientY)}),e.addEventListener("mouseleave",()=>{n.style.display="none"}),e.addEventListener("click",()=>{n.style.display="none"}),e.addEventListener("focus",()=>{n.textContent=t,n.style.display="block";const o=e.getBoundingClientRect();positionChartTooltip(n,r,o.left+o.width/2,o.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,r,n,t){const o=r.getBoundingClientRect(),i=14,a=4;e.style.left="0",e.style.top="0";const s=e.offsetWidth,l=e.offsetHeight;let c=n-o.left+i,d=t-o.top+i;c+s+a>o.width&&(c=n-o.left-i-s),d+l+a>o.height&&(d=t-o.top-i-l),c=Math.max(a,c),d=Math.max(a,d),e.style.left=`${c}px`,e.style.top=`${d}px`}export function robustFence(e,r=1.5){const n=e.filter(s=>Number.isFinite(s)).sort((s,l)=>s-l);if(n.length<8)return null;const t=s=>{const l=(n.length-1)*s,c=Math.floor(l),d=Math.ceil(l);return c===d?n[c]:n[c]+(n[d]-n[c])*(l-c)},o=t(.25),i=t(.75),a=i-o;return a===0?null:{lo:o-r*a,hi:i+r*a}}export function shouldIncludeLimitsByDefault(e,r,n,t,o=1/3){if(n===void 0&&t===void 0||!Number.isFinite(e)||!Number.isFinite(r))return!1;const i=Math.min(e,n??e),s=Math.max(r,t??r)-i;if(s<=0)return!0;const l=r-e;return l===0?!0:l/s>=o}export function resolveAxisRange(e){const{dataMin:r,dataMax:n,limitLow:t,limitHigh:o,includeLimits:i,clipOutliers:a,values:s}=e;let l=r,c=n,d=0;if(a&&s?.length){const u=robustFence(s);if(u){const w=Math.max(l,u.lo),C=Math.min(c,u.hi);C>w&&(d=s.filter(S=>Number.isFinite(S)&&(S<w||S>C)).length,l=w,c=C)}}i&&(t!==void 0&&(l=Math.min(l,t)),o!==void 0&&(c=Math.max(c,o))),l===c&&(l-=1,c+=1);const p=[];return t!==void 0&&t<l&&p.push({value:t,label:"LSL",side:"lo"}),t!==void 0&&t>c&&p.push({value:t,label:"LSL",side:"hi"}),o!==void 0&&o>c&&p.push({value:o,label:"USL",side:"hi"}),o!==void 0&&o<l&&p.push({value:o,label:"USL",side:"lo"}),{lo:l,hi:c,offAxis:p,clippedCount:d}}export function limitLabelSide(e,r,n,t,o,i=3){const a=o?-1:1;return(a<0?e-i-r>=n:e+i+r<=t)?a:-a}export function drawOffAxisLimits(e,r,n,t,o,i){if(r.length){e.save(),e.fillStyle=o,e.font=`${j(-1)}px system-ui, sans-serif`;for(const{value:a,label:s,side:l}of r){const c=t==="horizontal"?l==="lo"?"\u2190":"\u2192":l==="lo"?"\u2193":"\u2191",d=l==="lo"?`${c} ${s} ${i(a)}`:`${s} ${i(a)} ${c}`;t==="horizontal"?(e.textBaseline="top",e.textAlign=l==="lo"?"left":"right",e.fillText(d,l==="lo"?n.left+2:n.right-2,n.top+2)):(e.textBaseline=l==="lo"?"bottom":"top",e.textAlign="left",e.fillText(d,n.left+4,l==="lo"?n.bottom-2:n.top+2))}e.restore()}}
1
+ import{SHADOW as j,LEADING as H,wireControlHover as L,controlStyle as T,SPACE as m,RADIUS as E,fontPx as F,FONT as v,CLR as p,Z_BASE as U,menuLayerFor as X,wireListNavigation as Y,MENU_SEARCH_THRESHOLD as N,makeMenuSearchBox as K,markMenuTrigger as M,saveImageBlob as Z,openReparentedModal as J}from"../toolbar.js";import{ICONS as P}from"../icons.js";import{fmt as Q,fmtColorbarAxis as ee}from"../../renderer/fmt.js";import{markedTestLabel as te,DERIVED_LANE_PAD as ne}from"../../renderer/testLabel.js";export function openChartExpandModal(e,i,n={}){for(const a of e.querySelectorAll('div[style*="position: absolute"]'))a.style.display==="block"&&a.style.pointerEvents==="none"&&(a.style.display="none");const t=e.querySelector("[data-wmap-chart-expand]"),o=e.getAttribute("style")??"",r=e.dataset.wmapChartGrow??"plot";Object.assign(e.style,{flex:"1",minHeight:"0",alignSelf:"stretch",border:"none",borderRadius:"0"}),e.dataset.wmapExpanded="1";const l=J([e],{title:i,anchor:n.anchor,ownerDocument:e.ownerDocument,boxSize:oe[r],onClosed:()=>{delete e.dataset.wmapExpanded,e.setAttribute("style",o),t&&(t.style.display="flex"),n.onClosed?.()}});if(!l){delete e.dataset.wmapExpanded,e.setAttribute("style",o);return}if(t&&(t.style.display="none"),r==="rows"){const a=()=>{const s=Number(e.dataset.wmapChartFullH);if(!s)return;const c=l.box.offsetHeight-l.contentWrap.clientHeight;l.setNaturalHeight(`min(90vh, ${Math.ceil(c+s)}px)`)};a(),(e.ownerDocument.defaultView??window).requestAnimationFrame(a)}}const oe={plot:{width:"min(94vw, 1100px)",height:"min(90vh, 760px)"},rows:{width:"min(94vw, 960px)",height:"min(90vh, 700px)"},square:{width:"min(94vw, 820px)",height:"min(90vh, 860px)"}},ie={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040",accent:"#1a66cc",limitLine:"#b45309"},W={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border",accent:"icon-active",limitLine:"warn-text"};export function resolveChartCanvasColors(e){const i=getComputedStyle(e),n={};for(const t of Object.keys(W)){const o=i.getPropertyValue(`--wmap-${W[t]}`).trim();n[t]=o||ie[t]}return n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,i){const n=e.ownerDocument.defaultView??window;let t=0;const o=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,i()}))});return o.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,o.disconnect()}}}export function growCardToFitContent(e,i,n){const t=`${R(e,i)+n}px`;e.style.minHeight!==t&&(e.style.minHeight=t)}function R(e,i){const n=e.ownerDocument.defaultView??window,t=n.getComputedStyle(e);let o=0;for(const r of Array.from(e.children)){if(r===i)continue;const l=r,a=n.getComputedStyle(l);a.display==="none"||a.position==="absolute"||a.position==="fixed"||(o+=l.offsetHeight+(parseFloat(a.marginTop)||0)+(parseFloat(a.marginBottom)||0))}return t.boxSizing==="border-box"&&(o+=(parseFloat(t.paddingTop)||0)+(parseFloat(t.paddingBottom)||0)+(parseFloat(t.borderTopWidth)||0)+(parseFloat(t.borderBottomWidth)||0)),o}function _(e,i){const n=e.ownerDocument.defaultView??window;let t=0;for(const o of Array.from(e.children)){if(o===i)continue;const r=o,l=n.getComputedStyle(r);l.display==="none"||l.position==="absolute"||l.position==="fixed"||(t+=r.offsetHeight+(parseFloat(l.marginTop)||0)+(parseFloat(l.marginBottom)||0))}return t}export const WAFER_MENU_HINT="<br><em>right-click to chart this wafer</em>";export function populationPhrase(e,i,n){const t=e.toLocaleString(),o=e===1?"die":"dies";return n?e===i?`${t} ${o} ${n}`:`${t} of ${i.toLocaleString()} dies ${n} (partial and edge-excluded dies left out)`:`${t} ${o}`}export function setChartGrow(e,i){e.dataset.wmapChartGrow=i}export function isExpandedCard(e){return e.dataset.wmapExpanded==="1"}export function bodyRoom(e,i,n){const o=(e.ownerDocument.defaultView??window).getComputedStyle(e),r=e.clientHeight-(parseFloat(o.paddingTop)||0)-(parseFloat(o.paddingBottom)||0),l=o.boxSizing==="border-box"?(parseFloat(o.paddingTop)||0)+(parseFloat(o.paddingBottom)||0)+(parseFloat(o.borderTopWidth)||0)+(parseFloat(o.borderBottomWidth)||0):0;return r-(R(e,i)-l)-(n?_(i,n):0)}export function fitRowsHeight(e,i,n,t,o=0){if(growCardToFitContent(e,i,o+n),e.dataset.wmapChartFullH=String(Math.ceil(R(e,i)+o+t)),!isExpandedCard(e))return n;const r=bodyRoom(e,i)-o;return Math.max(n,Math.min(t,Math.floor(r)))}export function chartFillHeight(e,i,n,t){const o=_(i,n);return growCardToFitContent(e,i,o+t),Math.max(t,i.clientHeight-o)}export function chartDpr(e){return(e.ownerDocument.defaultView??window).devicePixelRatio||1}export function prepareCanvas(e,i,n,t){const o=chartDpr(e);e.width=Math.max(1,Math.floor(n*o)),e.height=Math.max(1,Math.floor(t*o)),e.style.width=`${n}px`,e.style.height=`${t}px`;const r=e.getContext("2d");return r?(r.setTransform(o,0,0,o,0,0),r.clearRect(0,0,n,t),r.font=`${F(-1)}px system-ui, sans-serif`,r.textBaseline="middle",{ctx:r,theme:resolveChartCanvasColors(i)}):null}export function applyCanvasFlow(e,i=0){let n;if(typeof i=="number")n=i;else{const t=(i.ownerDocument.defaultView??window).getComputedStyle(i);n=i.offsetTop+i.offsetHeight+(parseFloat(t.marginBottom)||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}`:Q(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,i,n){if(!i&&n===void 0)return{tick:formatValue,tickWithUnit:formatValue,unitLabel:""};const{tickFmt:t,axisLabel:o,scaledUnit:r}=ee(e,null,i,"engineering",n);return{tick:t,tickWithUnit:r?l=>`${t(l)} ${r}`:t,unitLabel:o}}const re=8;export const VERTICAL_TICK_SPACING_PX=28;export function horizontalTickSpacing(e,i,n){return(t,o)=>{const r=makeAxisFormat(i,n,t).tick;return o.reduce((l,a)=>Math.max(l,e.measureText(r(a)).width),0)+re}}export function drawAxisUnit(e,i,n,t,o){const r={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font=`${F(-1)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="middle",e.fillStyle=o,e.fillText(`(${i})`,n,t),Object.assign(e,r)}export function saveCanvasPng(e,i,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const o=t.getContext("2d");o.fillStyle="#ffffff",o.fillRect(0,0,t.width,t.height),o.drawImage(e,0,0),t.toBlob(r=>{r&&Z(r,i,n)},"image/png")}const z="10px";export function chartSwatchCss(e,i="solid"){return["display:inline-block",`width:${z}`,`height:${z}`,`border-radius:${E.pill}`,`background:${i==="outline"?"transparent":e}`,`border:1px ${i==="outline"?"dashed":"solid"} ${e}`,"box-sizing:border-box","flex-shrink:0"].join(";")}const le="0.35";export function makeSeriesLegendItem(e,i,n=document){const t=n.createElement("button");t.type="button",Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:m.xs,padding:`${m.xs} ${m.md}`,borderRadius:E.pill,border:`1px solid ${p.menuBorder}`,background:"none",cursor:"pointer",fontSize:v.body,color:p.text,whiteSpace:"nowrap"}),L(t,"bare");const o=n.createElement("span");return o.style.cssText=chartSwatchCss(i),t.append(o,n.createTextNode(e)),{el:t,setState({selected:r,dimmed:l}){t.style.borderColor=r?p.iconActive:p.menuBorder,t.style.fontWeight=r?"600":"400",t.style.opacity=l?le:"1",t.setAttribute("aria-pressed",r?"true":"false")}}}export function cardFrameStyle(){return{background:p.menuBg,border:`1px solid ${p.menuBorder}`,borderRadius:E.container,padding:m.xl,fontVariantNumeric:"tabular-nums"}}export function cardShell(e,i,n=document){const t=n.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",...cardFrameStyle(),minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const o=t.ownerDocument.createElement("div");Object.assign(o.style,{display:"flex",alignItems:"center",gap:m.sm,paddingBottom:"6px",marginBottom:m.md,borderBottom:`1px solid ${p.menuBorder}`});const r=t.ownerDocument.createElement("div");r.textContent=e,Object.assign(r.style,{color:p.value,fontSize:v.heading,fontWeight:"600",flex:"1"}),o.appendChild(r);const l=makeTooltip(t),a=t.ownerDocument.createElement("button");a.type="button",a.setAttribute("aria-label","Save as PNG"),a.innerHTML=P.download,Object.assign(a.style,{...T("outlined"),color:p.label,padding:"0",width:"22px",height:"22px",lineHeight:H.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(a),attachChartTip(a,t,l,"Save as PNG"),a.addEventListener("click",()=>{const f=t.querySelector("canvas");f&&saveCanvasPng(f,e,i)}),o.appendChild(a);const s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Expand"),s.innerHTML=P.expand,Object.assign(s.style,{...T("outlined"),color:p.label,padding:"0",width:"22px",height:"22px",lineHeight:H.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(s),attachChartTip(s,t,l,"Expand"),s.dataset.wmapChartExpand="1",s.addEventListener("click",()=>openChartExpandModal(t,r.textContent??e)),o.appendChild(s),t.appendChild(o);const c=t.ownerDocument.createElement("div");Object.assign(c.style,{display:"flex",gap:m.sm,marginBottom:m.md,flexWrap:"wrap",alignItems:"center"}),t.appendChild(c);const d=t.ownerDocument.createElement("div");return Object.assign(d.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(d),{card:t,heading:r,controlsRow:c,body:d}}export function makeSegmented(e,i,n,t=document,o=!1){const r=t.createElement("div");r.setAttribute("role","radiogroup"),Object.assign(r.style,{display:"inline-flex",border:`1px solid ${p.menuBorder}`,borderRadius:E.control,overflow:"hidden"});const l=`seg-${Math.random().toString(36).slice(2,9)}`,a=[];return e.forEach(([s,c],d)=>{const f=t.createElement("label");Object.assign(f.style,{display:"inline-flex",alignItems:"center",fontSize:v.body,padding:o?"1px 6px":"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:d>0?`1px solid ${p.menuBorder}`:"none"}),L(f,"bare");const u=t.createElement("input");u.type="radio",u.name=l,u.value=s,u.checked=s===i,Object.assign(u.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const w=()=>{f.style.background=u.checked?p.bgActive:p.menuBg,f.style.color=u.checked?p.iconActive:p.text,f.dataset.on=u.checked?"true":"false"};a.push(w),w(),u.addEventListener("change",()=>{if(u.checked){for(const C of a)C();n(s)}}),f.append(u,t.createTextNode(c)),r.appendChild(f)}),r}export function makeBackButton(e,i=document){const n=i.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{...T("outlined"),background:p.menuBg,color:p.label}),L(n),n.addEventListener("click",e),n}export function renderEmptyState(e,i,n){const t=e.ownerDocument.createElement("div");t.textContent=i,Object.assign(t.style,{color:p.label,fontSize:v.body,padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeListSelect(e,i,n,t={}){const{maxWidth:o="200px",ownerDocument:r=document,ariaLabel:l,emptyText:a,searchPlaceholder:s="Filter\u2026",hook:c}=t;let d=i;const f=b=>e.find($=>$.value===b)?.label??"",u=r.createElement("button");u.type="button",c&&(u.dataset.wmapSelect=c),l&&u.setAttribute("aria-label",l),Object.assign(u.style,{fontSize:v.body,padding:`${m.xxs} ${m.sm}`,background:p.menuBg,color:p.text,border:`1px solid ${p.menuBorder}`,borderRadius:E.control,maxWidth:o,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:m.sm,justifyContent:"space-between"}),L(u),M(u,!1);const w=r.createElement("span");Object.assign(w.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const C=r.createElement("span");C.textContent="\u25BE",C.style.flex="0 0 auto",u.append(w,C);const S=()=>{w.textContent=e.length===0?a??"(none)":f(d)||a||"(none)"};if(S(),e.length===0)return u.disabled=!0,u.style.opacity="0.6",u.style.cursor="default",Object.defineProperty(u,"value",{get:()=>d,set:b=>{d=b,S()}});let x=null;const k=()=>{x&&(x.remove(),x=null,M(u,!1))},A=()=>{k(),u.focus()};function V(){const b=u.getBoundingClientRect(),$=r.defaultView??window,D=Math.max(b.width,220),G=Math.min(b.left,Math.max(4,($.innerWidth??1/0)-D-4));x=r.createElement("div"),x.setAttribute("role","listbox"),x.setAttribute("aria-label",l??"Options"),Object.assign(x.style,{position:"fixed",top:`${b.bottom+4}px`,left:`${G}px`,background:p.menuBg,border:`1px solid ${p.menuBorder}`,borderRadius:E.control,boxShadow:j.menu,zIndex:U,minWidth:`${D}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const B=[],O=()=>B.filter(h=>h.row.style.display!=="none").map(h=>h.row);if(e.length>N){const h=K(y=>{for(const g of B)g.row.style.display=g.label.includes(y)?"":"none"},s,r);Object.assign(h.style,{position:"sticky",top:"0",zIndex:"1",background:p.menuBg}),h.addEventListener("keydown",y=>{if(y.key==="Escape"){A();return}y.key==="ArrowDown"&&(y.preventDefault(),O()[0]?.focus())}),x.appendChild(h)}for(const h of e){const y=h.value===d,g=r.createElement("div");g.textContent=h.label,g.setAttribute("role","option"),g.setAttribute("aria-selected",y?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:`${m.sm} ${m.xl}`,fontSize:v.body,cursor:"pointer",color:y?p.iconActive:p.text,fontWeight:y?"700":"400",background:y?p.menuActive:"transparent",whiteSpace:"nowrap"}),g.addEventListener("mouseenter",()=>{y||(g.style.background=p.menuHover)}),g.addEventListener("mouseleave",()=>{y||(g.style.background="transparent")}),g.addEventListener("click",q=>{q.stopPropagation(),d=h.value,S(),A(),n(h.value)}),x.appendChild(g),B.push({row:g,label:h.label.toLowerCase()})}if(Y(x,O,A),X(u).appendChild(x),M(u,!0),e.length<=N){const h=e.findIndex(y=>y.value===d);O()[h>=0?h:0]?.focus()}}u.addEventListener("click",b=>{if(b.stopPropagation(),x){k();return}V()});const I=b=>{if(!u.isConnected){r.removeEventListener("click",I);return}x&&!x.contains(b.target)&&!u.contains(b.target)&&k()};return r.addEventListener("click",I),Object.defineProperty(u,"value",{get:()=>d,set:b=>{d=b,S()}})}export function testOptionLabels(e){const i=e.some(n=>n.derived);return e.map(n=>(i&&!n.derived?ne:"")+te(n,n.testNumber))}export function makeTestSelect(e,i,n,t={}){const{maxWidth:o="200px",emptyText:r="No parametric tests",ownerDocument:l=document}=t;return makeListSelect(testOptionLabels(e).map((a,s)=>({value:String(e[s].testNumber),label:a})),i!==null?String(i):"",a=>n(Number(a)),{maxWidth:o,ownerDocument:l,ariaLabel:"Test",emptyText:r,searchPlaceholder:"Filter tests\u2026"})}export function makeLinkedAxisPrefs(e,i,n,t=document){let o=i?.includeLimits,r=i?.clipOutliers??!1;const l=()=>n({includeLimits:o,clipOutliers:r});return{get:()=>({includeLimits:o,clipOutliers:r}),sync(a,s){e.innerHTML="",s&&e.appendChild(makeToggle("Axis includes limits",a,c=>{o=c,l()},t)),e.appendChild(makeToggle("Clip outliers",r,c=>{r=c,l()},t))},set(a){return a.includeLimits===o&&a.clipOutliers===r?!1:(o=a.includeLimits,r=a.clipOutliers,!0)}}}export function makeLinkedTestSelect(e,i,n,t={}){let o=i;const r=makeTestSelect(e,o,l=>{l!==o&&(o=l,n(l))},t);return{el:r,get:()=>o,set(l){return o===l||!e.some(a=>a.testNumber===l)?!1:(o=l,r.value=String(l),!0)}}}export const ALL_GROUPS="\0all";export function makeLinkedGroupSelect(e,i,n,t,o={}){let r=n;const l=d=>d??ALL_GROUPS,a=d=>d===ALL_GROUPS?null:d,s=makeLabeledSelect(i,[{value:ALL_GROUPS,label:"All groups"},...e.map(d=>({value:d,label:d}))],l(r),d=>{const f=a(d);f!==r&&(r=f,t(f))},{...o,hook:"group-scope"}),c=s.querySelector('[data-wmap-select="group-scope"]');return{el:s,get:()=>r,set(d){return r===d||d!==null&&!e.includes(d)?!1:(r=d,c&&(c.value=l(d)),!0)}}}export function makeWaferSelect(e,i,n,t={}){const{maxWidth:o="160px",allLabel:r="All wafers",ownerDocument:l=document}=t,a="\0all";return makeListSelect([{value:a,label:r},...e.map((s,c)=>({value:String(c),label:s.label??`#${c}`}))],i===null?a:String(i),s=>n(s===a?null:Number(s)),{maxWidth:o,ownerDocument:l,ariaLabel:r,searchPlaceholder:"Filter wafers\u2026"})}export function makeToggle(e,i,n,t=document){const o=t.createElement("label");Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:m.xs,fontSize:v.body,color:p.label,cursor:"pointer",userSelect:"none"}),L(o,"bare");const r=t.createElement("input");return r.type="checkbox",r.checked=i,r.style.cssText="margin:0;cursor:pointer;",r.addEventListener("change",()=>n(r.checked)),o.append(r,t.createTextNode(e)),o}export function makeLabeledSelect(e,i,n,t,o={}){const{maxWidth:r="160px",hook:l,ownerDocument:a=document}=o,s=a.createElement("label");s.textContent=e,Object.assign(s.style,{color:p.label,fontSize:v.body,display:"flex",alignItems:"center",gap:m.xs});const c=makeListSelect(i,n,t,{maxWidth:r,ownerDocument:a,hook:l,ariaLabel:e.replace(/:\s*$/,"")});return s.appendChild(c),s}export function makeChartGridWrap(e=document){const i=e.createElement("div");return i.dataset.wmapChartGrid="1",Object.assign(i.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:m.lg,flex:"0 0 auto"}),i}export function makeTooltip(e){const i=e.ownerDocument.createElement("div");return Object.assign(i.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:"rgba(30, 32, 40, 0.93)",color:"#f0f0f2",border:"1px solid rgba(255,255,255,0.10)",borderRadius:E.control,padding:`${m.sm} ${m.xl}`,fontSize:v.sub,fontFamily:v.family,maxWidth:"320px",whiteSpace:"normal",overflowWrap:"break-word",boxShadow:j.menu}),e.appendChild(i),i}export function attachChartTip(e,i,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",o=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,i,o.clientX,o.clientY)}),e.addEventListener("mousemove",o=>{n.style.display==="block"&&positionChartTooltip(n,i,o.clientX,o.clientY)}),e.addEventListener("mouseleave",()=>{n.style.display="none"}),e.addEventListener("click",()=>{n.style.display="none"}),e.addEventListener("focus",()=>{n.textContent=t,n.style.display="block";const o=e.getBoundingClientRect();positionChartTooltip(n,i,o.left+o.width/2,o.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,i,n,t){const o=i.getBoundingClientRect(),r=14,l=4;e.style.left="0",e.style.top="0";const a=e.offsetWidth,s=e.offsetHeight;let c=n-o.left+r,d=t-o.top+r;c+a+l>o.width&&(c=n-o.left-r-a),d+s+l>o.height&&(d=t-o.top-r-s),c=Math.max(l,c),d=Math.max(l,d),e.style.left=`${c}px`,e.style.top=`${d}px`}export function robustFence(e,i=1.5){const n=Float64Array.from(e.filter(a=>Number.isFinite(a)));if(n.sort(),n.length<8)return null;const t=a=>{const s=(n.length-1)*a,c=Math.floor(s),d=Math.ceil(s);return c===d?n[c]:n[c]+(n[d]-n[c])*(s-c)},o=t(.25),r=t(.75),l=r-o;return l===0?null:{lo:o-i*l,hi:r+i*l}}export function shouldIncludeLimitsByDefault(e,i,n,t,o=1/3){if(n===void 0&&t===void 0||!Number.isFinite(e)||!Number.isFinite(i))return!1;const r=Math.min(e,n??e),a=Math.max(i,t??i)-r;if(a<=0)return!0;const s=i-e;return s===0?!0:s/a>=o}export function resolveAxisRange(e){const{dataMin:i,dataMax:n,limitLow:t,limitHigh:o,includeLimits:r,clipOutliers:l,values:a}=e;let s=i,c=n,d=0;if(l&&a?.length){const u=robustFence(a);if(u){const w=Math.max(s,u.lo),C=Math.min(c,u.hi);C>w&&(d=a.filter(S=>Number.isFinite(S)&&(S<w||S>C)).length,s=w,c=C)}}r&&(t!==void 0&&(s=Math.min(s,t)),o!==void 0&&(c=Math.max(c,o))),s===c&&(s-=1,c+=1);const f=[];return t!==void 0&&t<s&&f.push({value:t,label:"LSL",side:"lo"}),t!==void 0&&t>c&&f.push({value:t,label:"LSL",side:"hi"}),o!==void 0&&o>c&&f.push({value:o,label:"USL",side:"hi"}),o!==void 0&&o<s&&f.push({value:o,label:"USL",side:"lo"}),{lo:s,hi:c,offAxis:f,clippedCount:d}}export function limitLabelSide(e,i,n,t,o,r=3){const l=o?-1:1;return(l<0?e-r-i>=n:e+r+i<=t)?l:-l}export function drawOffAxisLimits(e,i,n,t,o,r){if(i.length){e.save(),e.fillStyle=o,e.font=`${F(-1)}px system-ui, sans-serif`;for(const{value:l,label:a,side:s}of i){const c=t==="horizontal"?s==="lo"?"\u2190":"\u2192":s==="lo"?"\u2193":"\u2191",d=s==="lo"?`${c} ${a} ${r(l)}`:`${a} ${r(l)} ${c}`;t==="horizontal"?(e.textBaseline="top",e.textAlign=s==="lo"?"left":"right",e.fillText(d,s==="lo"?n.left+2:n.right-2,n.top+2)):(e.textBaseline=s==="lo"?"bottom":"top",e.textAlign="left",e.fillText(d,n.left+4,s==="lo"?n.bottom-2:n.top+2))}e.restore()}}