@wafertools/wafermap 0.30.4 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/AGENTS.md +9 -19
  2. package/CHANGELOG.md +93 -1
  3. package/README.md +2 -2
  4. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +89 -6
  6. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/scatter.d.ts +11 -1
  9. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  11. package/dist/packages/canvas-adapter/dieList.d.ts +1 -1
  12. package/dist/packages/canvas-adapter/exportName.d.ts +1 -12
  13. package/dist/packages/canvas-adapter/exportName.js +1 -1
  14. package/dist/packages/canvas-adapter/index.d.ts +2 -3
  15. package/dist/packages/canvas-adapter/index.js +1 -1
  16. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  17. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +4 -7
  18. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  19. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +15 -30
  20. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  21. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  22. package/dist/packages/canvas-adapter/userGuideHtml.js +14 -10
  23. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  24. package/dist/packages/canvas-adapter/version.js +1 -1
  25. package/dist/packages/core/aggregates.d.ts +0 -6
  26. package/dist/packages/core/aggregates.js +1 -1
  27. package/dist/packages/core/dies.d.ts +0 -2
  28. package/dist/packages/core/dies.js +1 -1
  29. package/dist/packages/core/index.d.ts +8 -10
  30. package/dist/packages/core/index.js +1 -1
  31. package/dist/packages/core/stdf.d.ts +9 -0
  32. package/dist/packages/core/stdf.js +1 -0
  33. package/dist/packages/core/transforms.d.ts +0 -23
  34. package/dist/packages/core/transforms.js +1 -1
  35. package/dist/packages/renderer/buildView.d.ts +1 -9
  36. package/dist/packages/renderer/buildView.js +1 -1
  37. package/dist/packages/renderer/buildWaferMap.d.ts +17 -25
  38. package/dist/packages/renderer/buildWaferMap.js +1 -1
  39. package/dist/packages/renderer/colorMap.d.ts +0 -2
  40. package/dist/packages/renderer/colorMap.js +1 -1
  41. package/dist/packages/renderer/deprecate.d.ts +1 -1
  42. package/dist/packages/renderer/deprecate.js +1 -1
  43. package/dist/packages/renderer/derivedTests/apply.js +1 -1
  44. package/dist/packages/renderer/index.d.ts +6 -5
  45. package/dist/packages/renderer/index.js +1 -1
  46. package/dist/packages/stats/clusterDetection.d.ts +2 -1
  47. package/dist/packages/stats/facets.d.ts +1 -1
  48. package/dist/packages/stats/index.d.ts +6 -16
  49. package/dist/packages/stats/index.js +1 -1
  50. package/dist/packages/stats/patternClassification.d.ts +2 -1
  51. package/dist/packages/stats/regions.d.ts +2 -1
  52. package/dist/packages/stats/renderFindingsReport.d.ts +0 -4
  53. package/dist/packages/stats/renderFindingsReport.js +1 -21
  54. package/package.json +2 -2
  55. package/dist/packages/canvas-adapter/deprecated.d.ts +0 -7
  56. package/dist/packages/canvas-adapter/deprecated.js +0 -1
  57. package/dist/packages/core/deprecated.d.ts +0 -60
  58. package/dist/packages/core/deprecated.js +0 -1
  59. package/dist/packages/renderer/deprecated.d.ts +0 -45
  60. package/dist/packages/renderer/deprecated.js +0 -1
  61. package/dist/packages/stats/deprecated.d.ts +0 -85
  62. package/dist/packages/stats/deprecated.js +0 -1
package/AGENTS.md CHANGED
@@ -19,7 +19,7 @@ loudly over guessing.
19
19
 
20
20
  ### Entry points
21
21
 
22
- - `@wafertools/wafermap` — `buildWaferMap()`, geometry, `registerBinColorScheme()` / `registerValueColorScheme()`. Pure, no DOM, server-safe.
22
+ - `@wafertools/wafermap` — `buildWaferMap()`, `registerBinColorScheme()` / `registerValueColorScheme()`. Pure, no DOM, server-safe.
23
23
  - `@wafertools/wafermap/render` — `renderWaferMap()`, `renderWaferGallery()`. Needs the DOM.
24
24
  - `@wafertools/wafermap/stats` — `analyzeWaferMap()`, `analyzeWaferLot()`. Pure analysis.
25
25
  - `@wafertools/wafermap/worker` — `createWafermapWorker()` for off-main-thread builds.
@@ -27,22 +27,13 @@ loudly over guessing.
27
27
  Default path: `buildWaferMap()` once when data loads, then `renderWaferMap()` for a
28
28
  single wafer or `renderWaferGallery()` for several.
29
29
 
30
- **The types still export a large deprecated surface that is removed in 0.31.0 — do not
31
- reach into it just because autocomplete offers it.** Four groups, all replaced by the
32
- default path above:
30
+ **There is no low-level drawing pipeline, chart-data builder or region builder to reach
31
+ for.** The library draws through the renderers, and every figure it shows (yield, bin
32
+ counts, region yield, per-test statistics, capability) comes back from
33
+ `analyzeWaferMap()` / `analyzeWaferLot()`.
33
34
 
34
- - the low-level drawing pipeline — `buildView()`, `toCanvas()`, `createWafer()`,
35
- `generateDies()`, and the geometry and transform helpers around them;
36
- - the chart-data builders — `buildYieldData()`, `buildCorrelationMatrix()`,
37
- `buildTestBoxplotData()` and the rest: these were the internals of the Insights tab,
38
- which the renderers now mount for you (see below);
39
- - the region builders — `buildRingRegions()`, `buildQuadrantRegions()` and friends:
40
- region yield comes back from `analyzeWaferMap()`;
41
- - per-die and per-colour helpers — `getDieTestValue()`, `buildHoverText()`,
42
- `resolveBinColors()`, `getValueColorScheme()`, `valueToViridis()`.
43
-
44
- If the only way to do something is through one of these, that is a library gap worth
45
- reporting, not a pattern to build on.
35
+ If the only way to do something is to rebuild one of those pieces, that is a library gap
36
+ worth reporting, not a pattern to build on.
46
37
 
47
38
  ### Traps that produce silently wrong maps
48
39
 
@@ -124,9 +115,8 @@ reporting, not a pattern to build on.
124
115
  `insights: { enabled: true }` and it mounts the chart suite (yield, bin pareto,
125
116
  boxplot, histogram, correlation, scatter, capability, and one card per
126
117
  `insights.sweeps` entry). `renderWaferGallery` takes the same option across a whole
127
- lot. Supply or replace the analysis later with `setStatsSummary()`. Hand-building
128
- those charts is what the deprecated chart-data builders were for, and they go in
129
- 0.31.0. Charting a selection or one wafer (right-click → histogram, capability,
118
+ lot. Supply or replace the analysis later with `setStatsSummary()`. There is no
119
+ chart-data API to hand-build them from. Charting a selection or one wafer (right-click → histogram, capability,
130
120
  sweeps) is built in too, with no wiring.
131
121
  - **A value computed from other tests is a derived test, not a host-side column.** Pass
132
122
  `derivedTests` (a `TestDef` plus an `expression`, e.g. `'abs(t[1020] - t[1010])'`) to
package/CHANGELOG.md CHANGED
@@ -22,7 +22,99 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
25
- ## [Unreleased]
25
+ ## [0.31.0] — 2026-09-25
26
+
27
+ ### Breaking
28
+
29
+ - **The 74 deprecated exports are removed**: the 73 deprecated in 0.30.0 (the low-level drawing
30
+ pipeline, the chart-data builders, the region builders and the helpers exported by accident)
31
+ and `renderFindingsReportHtml`, deprecated in 0.30.3. Each named its replacement in a console
32
+ notice. [Upgrading](https://wafertools.github.io/wafermap/upgrading/) lists every name with
33
+ what to use instead; the main replacements:
34
+
35
+ | Removed | Use instead |
36
+ |---|---|
37
+ | `resolveBinColors`, `getBinColorScheme` | `binColorsForMaps(results)`, or `getBinColors()` on a map or gallery controller |
38
+ | `valueToViridis`, `valueToGreyscale`, `getValueColorScheme` | `resolveValueColorFn(name, reversed)` |
39
+ | `buildYieldData`, `buildYieldDataCombined` | `lotYieldSeries` on `analyzeWaferLot`'s result |
40
+ | `buildBinParetoData`, `buildBinClusterData` | `stats.hardBinCounts`, `stats.softBinCounts` |
41
+ | `buildCapabilityData`, `buildTestBoxplotData`, `buildTestTrendData`, `trendCentre` | `stats.capability`, `stats.perTestStats`, `perWaferTestStats` (with `computePerTestStats`) |
42
+ | `buildTestPassRateData`, `hasJudgeableTests`, `computeFunctionalYield` | `stats.testSpecYield`, `stats.testFlagYield`, `stats.functionalYield` |
43
+ | `buildRegionYieldData`, `buildRingRegions`, `buildQuadrantRegions`, `classifyDie`, `getRingLabel` | `stats.regionYield` |
44
+ | `classifyPattern` | `stats.spatialPattern` |
45
+ | `renderSummaryReportHtml`, `renderLotSummaryReportHtml`, `renderFindingsReportHtml` | `renderWaferReportHtml(result, summary?)`, `renderLotReportHtml(results)` |
46
+ | `openHtmlReport` | `openReportModal(html)`, `setReportOpener` |
47
+ | `createWafer`, `generateDies`, `clipDiesToWafer` | `buildWaferMap({ layout: true, waferConfig, dieConfig })` |
48
+ | `aggregateValues`, `aggregateBinCounts`, `getUniqueBins` | `buildWaferMap`'s `lotStack` |
49
+ | `STANDARD_WAFER_DIAMETERS_MM`, `resolveGridPitch` | `buildWaferMap`'s `standardDiameters`; each die's `width`/`height` |
50
+ | `getDieTestValue`, `isParametricTest`, `isPositionedDie`, `metadataCategoricalValue` | `die.testValues?.[n]`, `testType !== 'F'`, `hasPosition`, `metadataDisplayValue` |
51
+
52
+ `buildView`, `toCanvas`, `buildHoverText`, `buildMapTitle`, the transform and `affine*`
53
+ helpers, the histogram, scatter and correlation data builders, the sector, reticle and
54
+ test-site region builders, `parseRegionKey`, `contrastTextColor`, `dieHasTestData`,
55
+ `resolveMetadataColumns`, `discoverDieMetadataKeys`, `buildDieListSection` and
56
+ `DEFAULT_FACET_CURATION` have no public replacement: the renderers and the analysis do that
57
+ work themselves.
58
+ - **63 types that belonged only to removed functions are removed with them**, among them the
59
+ chart-data types, the draw-list types (`View`, `ViewOptions`, `ViewRect` and the rest),
60
+ `PitchResult`, `ToCanvasResult`, `HitTarget`, `DieListOptions` and the geometry inputs
61
+ `WaferSpec`, `DieSpec` and `ReticleSpec`. Upgrading lists them all. `PlotMode` stays.
62
+ - **`buildWaferMap` takes one argument.** The second (`WaferMapOptions`) set only the starting
63
+ `result.plotMode`, which the Web Worker never passed, so a build on and off the main thread
64
+ could start in different modes. Set the starting mode with `renderWaferMap`'s or
65
+ `renderWaferGallery`'s `viewOptions.plotMode`.
66
+ - **`downloadFilename` is a prefix for every saved file**, on `renderWaferMap` and
67
+ `renderWaferGallery`: `<downloadFilename>_W05_hard-bin.png`, `<downloadFilename>_W05_die-list.csv`.
68
+ It applies to CSVs, charts, gallery cards and detached windows as well as the map's or gallery's
69
+ PNG, which it no longer names outright. Parts the prefix already names are not repeated. A host
70
+ that matched the exact name its `onSaveImage` receives should match on the prefix.
71
+ - **Values outside the STDF V4 ranges are treated as missing.** 0.30.4 reported them with the
72
+ `input-values-outside-stdf` warning and used them as given; `buildWaferMap` now leaves them out,
73
+ and the warning says what it did:
74
+ - a bin outside 0–32767 or not a whole number: the die has no bin (so it is neither pass nor fail);
75
+ - a coordinate outside ±32767 or not a whole number: the die has no position, in either axis;
76
+ - a test number outside 0–4294967295: that test is left out of every die and of `testDefs`;
77
+ - a test value that is not finite: that value is left out;
78
+ - a site number outside 0–255: the die has no site;
79
+ - a `waferConfig.orientation` other than 0, 90, 180 or 270 (or an equivalent such as −90): the
80
+ map is built at 0. `WaferConfig.orientation` is typed `0 | 90 | 180 | 270`.
81
+
82
+ The caller's input objects are not modified. A derived test whose `testNumber` is outside
83
+ 0–4294967295 is dropped with a `derived-test-invalid` warning.
84
+ - **`WaferViewOptions.showPartialDies` and `isYieldEligibleDie`'s `includePartial` are removed.**
85
+ No map `buildWaferMap` builds has partial dies, so neither had anything to act on. A saved
86
+ preference that still carries `showPartialDies` is ignored. Partial dies a host supplies
87
+ itself are drawn in muted grey and always left out of yield.
88
+
89
+ ### Added
90
+
91
+ - **Insights charts draw spec limits as well as test limits.** The boxplot, histogram,
92
+ wafer-to-wafer trend and scatter show a test's spec limits (`specLow`/`specHigh`, labelled
93
+ LSL/USL, long dashes) beside its test limits (Lo/Hi limit, short dashes). When a test has
94
+ both, a **Limits** choice (Test + spec, the default; Test limits; Spec limits; None) applies to
95
+ all four charts. Labels that would overlap move to a second row, **Axis includes limits**
96
+ covers every limit shown, and the scatter marks limits outside its plotted range at the edge,
97
+ as the other charts do. The wafer map still judges pass/fail by the test limits.
98
+ - **Chart gridlines are lighter** (the border colour at 40% opacity), so the data and the limit
99
+ lines stand out from the grid in every theme. The scatter draws its limits in the same amber
100
+ as the other charts, and its limit labels sit on a panel-coloured backing where lines cross
101
+ them.
102
+
103
+ ### Changed
104
+
105
+ - **The data-and-stats layer is ~49 KB gzipped**, from ~61 KB, with the removed exports gone.
106
+
107
+ ### Fixed
108
+
109
+ - **Charts opened from a gallery card's right-click menu are saved under the card's lot and
110
+ wafer**, as they are from a single map.
111
+
112
+ ### Documentation
113
+
114
+ - **New page, [Upgrading](https://wafertools.github.io/wafermap/upgrading/)**: what to change
115
+ for each breaking release, starting with 0.31.0.
116
+ - **Every version has a GitHub release**, created when its tag is pushed, with that version's
117
+ changelog section as its notes.
26
118
 
27
119
  ## [0.30.4] — 2026-09-25
28
120
 
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-~61%20kB-blue)
8
+ ![bundle](https://img.shields.io/badge/data%20layer%20min%2Bgz-~49%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 ~61 kB
16
+ Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~49 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
 
@@ -1 +1 @@
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 M,VALUE_WIDTH as vt,WAFER_MENU_HINT as kt,prepareCanvas as Mt}from"./chartShell.js";import{escHtml as L,maxOf as Lt,minOf as Ct}from"../../core/utils.js";const C=24,A=5,We=110,At=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:z}=u,_e=u.openActionLabel??"open that wafer",Ge=u.openActionLabel??"open this wafer",{card:a,heading:Ne,body:x,controlsRow:Q}=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,U=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),Q.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(),$(),!0)}const we=ut(ne,y,i=>{y=i,$(),u.onTestChange?.(i)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),qe=we.el;Q.appendChild(qe),Q.appendChild(mt("Log scale",ie,i=>{ie=i,$()},a.ownerDocument));const le=a.ownerDocument.createElement("span");Object.assign(le.style,{display:"inline-flex",gap:Be.lg,alignItems:"center"}),Q.appendChild(le);const se=dt(le,u.axisPrefs,i=>{u.onAxisPrefsChange?.(i),$()},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`):z&&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`+(U?` \xB7 axis clipped, ${U} value${U===1?"":"s"} outside`:"")}F(),Te();const D=rt(a);let re=null;function $(){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=M*2+Math.min(f.length,At)*(C+A)+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)),Me=Lt(fe.map(t=>t.max)),Le=i??Tt(ke,Me,V,j);se.sync(Le,V!==void 0||j!==void 0);const _=bt({dataMin:ke,dataMax:Me,limitLow:V,limitHigh:j,includeLimits:Le,clipOutliers:p,values:fe.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});U=_.clippedCount,F();const O=_.lo,K=_.hi,Qe=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 Ue(){const t=M+We,s=m.clientWidth-t-vt-M;return{plotX:t,plotMaxWidth:Math.max(10,s)}}function S(t,s,n){return J?s+(Math.log10(t)-ue)/Ce*n:s+(t-O)/Qe*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=M*2+f.length*(C+A)+Oe;x.style.maxHeight=`${xt(a,x,ve,s)}px`;const n=x.clientWidth,d=Mt(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}=Ue();f.forEach((o,T)=>{const v=M+T*(C+A),c=v+C/2;T===P&&(e.fillStyle=t.bgHover,e.fillRect(0,v-A/2,n,C+A)),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,M+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)),$e=S(o.min,l,r),Se=S(o.max,l,r),I=N($e),X=N(S(o.q1,l,r)),Ie=N(S(o.median,l,r)),pe=N(S(o.q3,l,r)),B=N(Se),Ze=$e<I,et=Se>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=M+f.length*(C+A);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,"Lo limit"],[j,"Hi limit"]]){if(o===void 0||Ve.has(o))continue;const v=S(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==="Lo limit");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=S(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-M+A/2)/(C+A));return s>=0&&s<f.length?s:-1}function G(t){return he(t,W,"engineering")}const E=!!g&&g.length>0&&b===null,Ae=t=>!E&&!!z&&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||Ae(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 ${L(te)} by wafer</em>`:Ae(n)?`<br><em>click to ${L(Ge)}</em>`:"",r=!E&&u.onWaferContextMenu&&h[n]?.waferIndex!==void 0?kt:"";D.innerHTML=`<strong>${L(e.label)}</strong> (${e.count} dies)<br>max ${L(G(e.max))}<br>q3 ${L(G(e.q3))}<br>median ${L(G(e.median))}<br>q1 ${L(G(e.q1))}<br>min ${L(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;z&&d!==void 0&&y!==null&&z(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()}$();function Ye(i){we.set(i)&&(y=i,$())}function ze(i){se.set(i)&&$()}return{card:a,setTest:Ye,setAxisPrefs:ze,destroy:()=>re?.disconnect()}}
1
+ import{buildTestBoxplotData as rt}from"../../stats/boxplot.js";import{SPACE as He,fontPx as _,FONT as ct,CLR as ft}from"../toolbar.js";import{fmt as ge}from"../../renderer/fmt.js";import{fitTicks as ut}from"../../renderer/axisTicks.js";import{QUANTITY as De}from"./palette.js";import{cardShell as mt,observeResize as dt,makeTooltip as xt,positionChartTooltip as pt,makeBackButton as ht,makeLinkedTestSelect as gt,makeToggle as bt,makeLinkedAxisPrefs as yt,renderEmptyState as Ee,fitRowsHeight as Tt,setChartGrow as wt,resolveChartCanvasColors as vt,makeAxisFormat as Oe,horizontalTickSpacing as kt,resolveAxisRange as Mt,shouldIncludeLimitsByDefault as Lt,drawOffAxisLimits as Ct,limitLabelSide as At,limitLines as We,limitExtent as $t,hasBothLimitKinds as St,stackLabelRows as It,strokeLimitLine as Bt,PADDING as M,VALUE_WIDTH as Rt,WAFER_MENU_HINT as Ht,prepareCanvas as Dt}from"./chartShell.js";import{escHtml as L,maxOf as Et,minOf as Ot}from"../../core/utils.js";const C=24,A=5,Pe=110,Wt=12,_e=20;export function renderBoxplotPanel(u){const{title:te="Test value distribution",items:Ge,testDefs:be,onSaveImage:Ne,groups:g,groupLabelText:ne="group",onOpen:Q}=u,Xe=u.openActionLabel??"open that wafer",qe=u.openActionLabel??"open this wafer",{card:a,heading:Ye,body:x,controlsRow:U}=mt(te,Ne,u.ownerDocument);wt(a,"rows"),a.style.flex="0 0 auto",x.style.flex="0 0 auto",a.style.alignSelf="start";const oe=be.filter(i=>i.testNumber!==void 0);let T=u.selectedTestNumber??oe[0]?.testNumber??null,ie=!1,F=0,b=null,D=null,le=null,ye=null;function ze(){const i=!g||g.length===0?"":b??"\0overview";if(le&&ye===i)return le;const p=!g||g.length===0?Ge:b!==null?g.find(v=>v.key===b)?.items??[]:g.map(v=>({label:v.key,dies:v.items.flatMap(S=>S.dies??[])}));return le=p,ye=i,p}function Te(){b!==null&&!D?(D=ht(()=>{we(null),u.onGroupChange?.(null)},a.ownerDocument),U.appendChild(D)):b===null&&D&&(D.remove(),D=null),Ye.textContent=b!==null?`${te} \u2014 ${ne}: ${b}`:te}function we(i){return b===i||i!==null&&!(g??[]).some(p=>p.key===i)?!1:(b=i,Te(),V(),$(),!0)}const ve=gt(oe,T,i=>{T=i,$(),u.onTestChange?.(i)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Qe=ve.el;U.appendChild(Qe),U.appendChild(bt("Log scale",ie,i=>{ie=i,$()},a.ownerDocument));const se=a.ownerDocument.createElement("span");Object.assign(se.style,{display:"inline-flex",gap:He.lg,alignItems:"center"}),U.appendChild(se);const ae=yt(se,u.axisPrefs,i=>{u.onAxisPrefsChange?.(i),$()},a.ownerDocument),re=a.ownerDocument.createElement("div");Object.assign(re.style,{color:ft.label,fontSize:ct.body,marginBottom:He.sm}),a.insertBefore(re,x);function V(){const i=!!g&&g.length>0&&b===null,p=[];i?p.push(`click a ${ne}'s box to see it by wafer`):Q&&p.push(`click a box to ${Xe}`);const v=p.length?`${p[0][0].toUpperCase()}${p[0].slice(1)} \xB7 `:"";re.textContent=`${v}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(F?` \xB7 axis clipped, ${F} value${F===1?"":"s"} outside`:"")}V(),Te();const E=xt(a);let ce=null;function $(){const{includeLimits:i,clipOutliers:p,limits:v}=ae.get();if(V(),x.innerHTML="",oe.length===0||T===null){Ee(x,"No parametric test data available for box plots.");return}const S=ze(),h=rt(S,T),j=be.find(t=>t.testNumber===T),O=j?.unit,fe=We(j,v),{lo:Ve,hi:je}=$t(fe);if(h.every(t=>t.count===0)){Ee(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 ke=M*2+Math.min(h.length,Wt)*(C+A)+_e;x.style.maxHeight=`${ke}px`,x.style.overflowY="auto",x.style.scrollbarGutter="stable";let G=-1;const ue=h.filter(t=>t.count>0),Me=Ot(ue.map(t=>t.min)),Le=Et(ue.map(t=>t.max)),Ce=i??Lt(Me,Le,Ve,je);ae.sync(Ce,We(j).length>0,St(j));const N=Mt({dataMin:Me,dataMax:Le,limits:fe,includeLimits:Ce,clipOutliers:p,values:ue.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});F=N.clippedCount,V();const W=N.lo,K=N.hi,Ke=K-W||1,J=ie&&W>0,me=J?Math.log10(W):0,Ae=(J?Math.log10(K):0)-me||1,de=Math.max(Math.abs(W),Math.abs(K));function Je(){const t=M+Pe,s=m.clientWidth-t-Rt-M;return{plotX:t,plotMaxWidth:Math.max(10,s)}}function I(t,s,n){return J?s+(Math.log10(t)-me)/Ae*n:s+(t-W)/Ke*n}function Ze(t,s){if(J){const d=[];for(let e=0;e<=4;e++)d.push(Math.pow(10,me+Ae*e/4));return{ticks:d,axis:Oe(de,O)}}const n=ut(W,K,s,kt(t,de,O));return{ticks:n.ticks,axis:Oe(de,O,n.step||void 0)}}function Z(){const t=vt(a),s=M*2+h.length*(C+A)+_e;x.style.maxHeight=`${Tt(a,x,ke,s)}px`;const n=x.clientWidth,d=Dt(m,a,n,s);if(!d)return;const{ctx:e}=d;e.font=`${_(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:c}=Je();h.forEach((o,f)=>{const y=M+f*(C+A),r=y+C/2;f===G&&(e.fillStyle=t.bgHover,e.fillRect(0,y-A/2,n,C+A)),e.fillStyle=t.text,e.textAlign="right";const ee=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(ee,M+Pe-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const it=l+c,q=at=>Math.min(it,Math.max(l,at)),Ie=I(o.min,l,c),Be=I(o.max,l,c),B=q(Ie),Y=q(I(o.q1,l,c)),Re=q(I(o.median,l,c)),he=q(I(o.q3,l,c)),R=q(Be),lt=Ie<B,st=Be>R,H=y+3,z=y+C-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(B,r),e.lineTo(Y,r),e.moveTo(he,r),e.lineTo(R,r);const k=3;lt?(e.moveTo(B+k,r-k),e.lineTo(B,r),e.lineTo(B+k,r+k)):(e.moveTo(B,H),e.lineTo(B,z)),st?(e.moveTo(R-k,r-k),e.lineTo(R,r),e.lineTo(R-k,r+k)):(e.moveTo(R,H),e.lineTo(R,z)),e.stroke(),e.fillStyle=De,e.globalAlpha=.35,e.fillRect(Y,H,Math.max(1,he-Y),z-H),e.globalAlpha=1,e.strokeStyle=De,e.strokeRect(Y,H,Math.max(1,he-Y),z-H),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Re,H),e.lineTo(Re,z),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(ge(o.median,O,"engineering"),l+c+10,r)});const w=M+h.length*(C+A);e.font=`${_(-1)}px system-ui, sans-serif`,Ct(e,N.offAxis,{left:l,right:l+c,top:0,bottom:w},"horizontal",t.limitLine,o=>ge(o,O,"engineering"));const et=new Set(N.offAxis.map(o=>o.value)),Se=fe.filter(o=>!et.has(o.value)).map(o=>{const f=I(o.value,l,c),y=e.measureText(o.label).width,r=At(f,y,l,l+c,o.end==="lo"),ee=r<0?f-3-y:f+3;return{l:o,x:f,dir:r,start:ee,end:ee+y}}),tt=It(Se),nt=_(-1)+2;Se.forEach(({l:o,x:f,dir:y},r)=>{Bt(e,o,t.limitLine,f,0,f,w),e.fillStyle=t.limitLine,e.textAlign=y<0?"right":"left",e.textBaseline="bottom",e.fillText(o.label,f+y*3,w-1-tt[r]*nt)}),e.font=`${_(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,w),e.lineTo(l+c,w),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:ot,axis:pe}=Ze(e,c);for(const o of ot){const f=I(o,l,c);e.beginPath(),e.moveTo(f,w),e.lineTo(f,w+4),e.stroke(),e.fillText(pe.tick(o),f,w+6)}pe.unitLabel&&(e.textAlign="left",e.font=`${_(-1)}px system-ui, sans-serif`,e.fillText(`(${pe.unitLabel})`,l+c+22,w+6),e.font=`${_(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function xe(t){const s=Math.floor((t-M+A/2)/(C+A));return s>=0&&s<h.length?s:-1}function X(t){return ge(t,O,"engineering")}const P=!!g&&g.length>0&&b===null,$e=t=>!P&&!!Q&&S[t]?.waferIndex!==void 0;m.addEventListener("mousemove",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top),d=n>=0&&h[n].count>0&&(P||$e(n));if(n!==G&&(G=n,m.style.cursor=d?"pointer":"default",Z()),n>=0&&h[n].count>0){const e=h[n],l=P?`<br><em>click to see this ${L(ne)} by wafer</em>`:$e(n)?`<br><em>click to ${L(qe)}</em>`:"",c=!P&&u.onWaferContextMenu&&S[n]?.waferIndex!==void 0?Ht:"";E.innerHTML=`<strong>${L(e.label)}</strong> (${e.count} dies)<br>max ${L(X(e.max))}<br>q3 ${L(X(e.q3))}<br>median ${L(X(e.median))}<br>q1 ${L(X(e.q1))}<br>min ${L(X(e.min))}${l}${c}`,E.style.display="block",pt(E,a,t.clientX,t.clientY)}else E.style.display="none"}),m.addEventListener("mouseleave",()=>{G!==-1&&(G=-1,Z()),E.style.display="none"}),m.addEventListener("click",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top);if(n===-1||h[n].count===0)return;if(P){const e=h[n].label;we(e),u.onGroupChange?.(e);return}const d=S[n]?.waferIndex;Q&&d!==void 0&&T!==null&&Q(d,T)}),m.addEventListener("contextmenu",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top),d=n===-1?void 0:S[n]?.waferIndex;P||d===void 0||!u.onWaferContextMenu||(E.style.display="none",u.onWaferContextMenu(d,T??void 0,t))}),ce?.disconnect(),ce=dt(a,()=>Z()),Z()}$();function Ue(i){ve.set(i)&&(T=i,$())}function Fe(i){ae.set(i)&&$()}return{card:a,setTest:Ue,setAxisPrefs:Fe,destroy:()=>ce?.disconnect()}}
@@ -43,7 +43,20 @@ export interface ChartCanvasColors {
43
43
  * limit line and its label need.
44
44
  */
45
45
  limitLine: string;
46
+ /**
47
+ * Gridlines: `border` at `GRID_ALPHA`. A gridline is a reading aid behind the
48
+ * data; drawn at full `border` strength (the axis colour) it competed with the
49
+ * points and with the limit lines, which in a theme with a strong border were
50
+ * hard to pick out from the grid at all.
51
+ */
52
+ grid: string;
46
53
  }
54
+ /** Opacity of gridlines relative to the border colour. */
55
+ export declare const GRID_ALPHA = 0.4;
56
+ /** `color` at `alpha` × its own opacity. Understands the forms a computed
57
+ * custom property holds — `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()` —
58
+ * and returns anything else unchanged rather than guessing. */
59
+ export declare function withAlpha(color: string, alpha: number): string;
47
60
  /** Resolve `--wmap-*` custom properties to concrete color strings for canvas
48
61
  * drawing. Call once per draw (not cached) so a live theme change is picked
49
62
  * up on the next redraw, matching `resolveCanvasTheme`'s own contract. */
@@ -349,6 +362,7 @@ export interface LinkedAxisPrefs {
349
362
  get(): {
350
363
  includeLimits: boolean | undefined;
351
364
  clipOutliers: boolean;
365
+ limits: LimitsShown;
352
366
  };
353
367
  /**
354
368
  * Rebuild the toggles for the state the panel has just RESOLVED.
@@ -356,9 +370,10 @@ export interface LinkedAxisPrefs {
356
370
  * Called from the redraw, because `includeLimits` is a tri-state: undefined
357
371
  * means "decide from the data" (`shouldIncludeLimitsByDefault`), and the
358
372
  * resolved answer is only known once the data range is. `hasLimits` decides
359
- * whether the limits toggle is offered at all.
373
+ * whether the limits toggle is offered at all, and `hasBothKinds` whether the
374
+ * "Limits:" choice between test and spec limits is.
360
375
  */
361
- sync(resolvedIncludeLimits: boolean, hasLimits: boolean): void;
376
+ sync(resolvedIncludeLimits: boolean, hasLimits: boolean, hasBothKinds?: boolean): void;
362
377
  /**
363
378
  * Adopt prefs chosen elsewhere. Returns false — changing nothing — when they
364
379
  * already match. Deliberately does NOT fire `onUserChange`, the same
@@ -490,7 +505,72 @@ export declare function positionChartTooltip(tooltip: HTMLElement, card: HTMLEle
490
505
  export interface AxisPrefs {
491
506
  includeLimits?: boolean;
492
507
  clipOutliers: boolean;
508
+ /** Which limits the charts draw. Undefined = `'both'`. */
509
+ limits?: LimitsShown;
493
510
  }
511
+ export type LimitKind = 'test' | 'spec';
512
+ /** The Insights "Limits" choice. */
513
+ export type LimitsShown = 'test' | 'spec' | 'both' | 'none';
514
+ export interface LimitLine {
515
+ value: number;
516
+ kind: LimitKind;
517
+ /** Which end of its pair: the low limit or the high one. */
518
+ end: 'lo' | 'hi';
519
+ label: string;
520
+ }
521
+ export declare const LIMIT_LABEL: Readonly<Record<LimitKind, {
522
+ lo: string;
523
+ hi: string;
524
+ }>>;
525
+ /** Canvas dash pattern per kind: short dashes for test limits, long for spec. */
526
+ export declare const LIMIT_DASH: Readonly<Record<LimitKind, number[]>>;
527
+ export declare const LIMITS_SHOWN_OPTIONS: ReadonlyArray<{
528
+ value: LimitsShown;
529
+ label: string;
530
+ }>;
531
+ type LimitSource = {
532
+ limitLow?: number;
533
+ limitHigh?: number;
534
+ specLow?: number;
535
+ specHigh?: number;
536
+ } | undefined;
537
+ /** Whether a test has both kinds of limit — the only case with a choice to make. */
538
+ export declare function hasBothLimitKinds(def: LimitSource): boolean;
539
+ /**
540
+ * The limit lines to draw for a test.
541
+ *
542
+ * `'none'` draws nothing. Otherwise the chosen kinds are drawn — and when the
543
+ * test has none of the chosen kind but does have the other, the other is drawn
544
+ * rather than nothing: a test with only test limits must not look unlimited
545
+ * because another test's spec limits were chosen. Each line carries its own
546
+ * label, so which kind is on screen is never in doubt.
547
+ */
548
+ export declare function limitLines(def: LimitSource, shown?: LimitsShown): LimitLine[];
549
+ /** The span the lines cover, for axis-range decisions. */
550
+ export declare function limitExtent(lines: readonly LimitLine[]): {
551
+ lo?: number;
552
+ hi?: number;
553
+ };
554
+ /**
555
+ * Stack labels into rows so none overlaps another in the same row. Each label
556
+ * is an interval along the axis it is placed on; returns a row index per label
557
+ * (0 = the chart's usual label position), in input order. Greedy by position,
558
+ * so it is stable: labels that never collide all stay on row 0.
559
+ */
560
+ export declare function stackLabelRows(spans: ReadonlyArray<{
561
+ start: number;
562
+ end: number;
563
+ }>, gap?: number): number[];
564
+ /**
565
+ * `fillText` on a backing of `halo` (the panel colour), so a label stays
566
+ * readable where a gridline, a limit line or data runs under it. Uses the
567
+ * current font, alignment and baseline; `halo` undefined draws plain text.
568
+ */
569
+ export declare function fillTextOnHalo(ctx: CanvasRenderingContext2D, text: string, x: number, y: number, halo?: string): void;
570
+ /** Stroke one limit line in its kind's dash pattern. */
571
+ export declare function strokeLimitLine(ctx: CanvasRenderingContext2D, line: LimitLine, color: string, x0: number, y0: number, x1: number, y1: number): void;
572
+ /** The "Limits:" dropdown, offered only where a test has both kinds. */
573
+ export declare function makeLimitsSelect(current: LimitsShown, onChange: (v: LimitsShown) => void, ownerDocument?: Document): HTMLLabelElement;
494
574
  /** Robust outlier fence over a value list: Tukey's `Q1 − k·IQR … Q3 + k·IQR`.
495
575
  *
496
576
  * Deliberately NOT mean ± 3σ. σ is computed FROM the data including the
@@ -524,7 +604,7 @@ export interface AxisRange {
524
604
  * is merely off-screen is indistinguishable from a test having no limit. */
525
605
  offAxis: Array<{
526
606
  value: number;
527
- label: 'Lo limit' | 'Hi limit';
607
+ label: string;
528
608
  side: 'lo' | 'hi';
529
609
  }>;
530
610
  /** Values excluded by the robust fence, when clipping is on. */
@@ -542,8 +622,8 @@ export interface AxisRange {
542
622
  export declare function resolveAxisRange(opts: {
543
623
  dataMin: number;
544
624
  dataMax: number;
545
- limitLow?: number;
546
- limitHigh?: number;
625
+ /** The limit lines the chart draws (`limitLines`). */
626
+ limits?: readonly LimitLine[];
547
627
  includeLimits: boolean;
548
628
  clipOutliers?: boolean;
549
629
  /** Raw values, needed only when `clipOutliers` is set. */
@@ -580,5 +660,8 @@ export declare function drawOffAxisLimits(ctx: CanvasRenderingContext2D, offAxis
580
660
  right: number;
581
661
  top: number;
582
662
  bottom: number;
583
- }, orient: 'horizontal' | 'vertical', color: string, format: (v: number) => string): void;
663
+ }, orient: 'horizontal' | 'vertical', color: string, format: (v: number) => string,
664
+ /** Panel colour to back each marker with (`fillTextOnHalo`), for charts
665
+ * where lines can run under the markers. */
666
+ halo?: string): void;
584
667
  //# sourceMappingURL=chartShell.d.ts.map
@@ -1 +1 @@
1
- import{SHADOW as D,LEADING as j,wireControlHover as E,controlStyle as T,SPACE as m,RADIUS as k,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:${k.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:k.pill,border:`1px solid ${p.menuBorder}`,background:"none",cursor:"pointer",fontSize:v.body,color:p.text,whiteSpace:"nowrap"}),E(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:k.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:j.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),E(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:j.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),E(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:k.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"}),E(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}),E(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:k.control,maxWidth:o,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:m.sm,justifyContent:"space-between"}),E(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 L=()=>{x&&(x.remove(),x=null,M(u,!1))},A=()=>{L(),u.focus()};function V(){const b=u.getBoundingClientRect(),$=r.defaultView??window,I=Math.max(b.width,220),G=Math.min(b.left,Math.max(4,($.innerWidth??1/0)-I-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:k.control,boxShadow:D.menu,zIndex:U,minWidth:`${I}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){L();return}V()});const H=b=>{if(!u.isConnected){r.removeEventListener("click",H);return}x&&!x.contains(b.target)&&!u.contains(b.target)&&L()};return r.addEventListener("click",H),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"}),E(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:k.control,padding:`${m.sm} ${m.xl}`,fontSize:v.sub,fontFamily:v.family,maxWidth:"320px",whiteSpace:"normal",overflowWrap:"break-word",boxShadow:D.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:"Lo limit",side:"lo"}),t!==void 0&&t>c&&f.push({value:t,label:"Lo limit",side:"hi"}),o!==void 0&&o>c&&f.push({value:o,label:"Hi limit",side:"hi"}),o!==void 0&&o<s&&f.push({value:o,label:"Hi limit",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()}}
1
+ import{SHADOW as D,LEADING as j,wireControlHover as L,controlStyle as I,SPACE as m,RADIUS as C,fontPx as k,FONT as S,CLR as f,Z_BASE as X,menuLayerFor as K,wireListNavigation as Y,MENU_SEARCH_THRESHOLD as W,makeMenuSearchBox as Z,markMenuTrigger as R,saveImageBlob as J,openReparentedModal as Q}from"../toolbar.js";import{ICONS as _}from"../icons.js";import{minOf as ee,maxOf as te}from"../../core/utils.js";import{fmt as ne,fmtColorbarAxis as oe}from"../../renderer/fmt.js";import{markedTestLabel as ie,DERIVED_LANE_PAD as re}from"../../renderer/testLabel.js";export function openChartExpandModal(e,o,n={}){for(const s of e.querySelectorAll('div[style*="position: absolute"]'))s.style.display==="block"&&s.style.pointerEvents==="none"&&(s.style.display="none");const t=e.querySelector("[data-wmap-chart-expand]"),i=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=Q([e],{title:o,anchor:n.anchor,ownerDocument:e.ownerDocument,boxSize:le[r],onClosed:()=>{delete e.dataset.wmapExpanded,e.setAttribute("style",i),t&&(t.style.display="flex"),n.onClosed?.()}});if(!l){delete e.dataset.wmapExpanded,e.setAttribute("style",i);return}if(t&&(t.style.display="none"),r==="rows"){const s=()=>{const a=Number(e.dataset.wmapChartFullH);if(!a)return;const d=l.box.offsetHeight-l.contentWrap.clientHeight;l.setNaturalHeight(`min(90vh, ${Math.ceil(d+a)}px)`)};s(),(e.ownerDocument.defaultView??window).requestAnimationFrame(s)}}const le={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)"}};export const GRID_ALPHA=.4;export function withAlpha(e,o){const n=e.trim();let t,i,r,l=1;const s=n.match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i),a=n.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i);if(s){const d=s[1].length===3?s[1].split("").map(c=>c+c).join(""):s[1];[t,i,r]=[0,2,4].map(c=>parseInt(d.slice(c,c+2),16)),d.length===8&&(l=parseInt(d.slice(6,8),16)/255)}else if(a)[t,i,r]=[a[1],a[2],a[3]].map(Number),a[4]!==void 0&&(l=a[4].endsWith("%")?parseFloat(a[4])/100:Number(a[4]));else return e;return`rgba(${t}, ${i}, ${r}, ${+(l*o).toFixed(3)})`}const se={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 o=getComputedStyle(e),n={};for(const t of Object.keys(P)){const i=o.getPropertyValue(`--wmap-${P[t]}`).trim();n[t]=i||se[t]}return n.grid=withAlpha(n.border,GRID_ALPHA),n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,o){const n=e.ownerDocument.defaultView??window;let t=0;const i=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,o()}))});return i.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,i.disconnect()}}}export function growCardToFitContent(e,o,n){const t=`${M(e,o)+n}px`;e.style.minHeight!==t&&(e.style.minHeight=t)}function M(e,o){const n=e.ownerDocument.defaultView??window,t=n.getComputedStyle(e);let i=0;for(const r of Array.from(e.children)){if(r===o)continue;const l=r,s=n.getComputedStyle(l);s.display==="none"||s.position==="absolute"||s.position==="fixed"||(i+=l.offsetHeight+(parseFloat(s.marginTop)||0)+(parseFloat(s.marginBottom)||0))}return t.boxSizing==="border-box"&&(i+=(parseFloat(t.paddingTop)||0)+(parseFloat(t.paddingBottom)||0)+(parseFloat(t.borderTopWidth)||0)+(parseFloat(t.borderBottomWidth)||0)),i}function z(e,o){const n=e.ownerDocument.defaultView??window;let t=0;for(const i of Array.from(e.children)){if(i===o)continue;const r=i,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,o,n){const t=e.toLocaleString(),i=e===1?"die":"dies";return n?e===o?`${t} ${i} ${n}`:`${t} of ${o.toLocaleString()} dies ${n} (partial and edge-excluded dies left out)`:`${t} ${i}`}export function setChartGrow(e,o){e.dataset.wmapChartGrow=o}export function isExpandedCard(e){return e.dataset.wmapExpanded==="1"}export function bodyRoom(e,o,n){const i=(e.ownerDocument.defaultView??window).getComputedStyle(e),r=e.clientHeight-(parseFloat(i.paddingTop)||0)-(parseFloat(i.paddingBottom)||0),l=i.boxSizing==="border-box"?(parseFloat(i.paddingTop)||0)+(parseFloat(i.paddingBottom)||0)+(parseFloat(i.borderTopWidth)||0)+(parseFloat(i.borderBottomWidth)||0):0;return r-(M(e,o)-l)-(n?z(o,n):0)}export function fitRowsHeight(e,o,n,t,i=0){if(growCardToFitContent(e,o,i+n),e.dataset.wmapChartFullH=String(Math.ceil(M(e,o)+i+t)),!isExpandedCard(e))return n;const r=bodyRoom(e,o)-i;return Math.max(n,Math.min(t,Math.floor(r)))}export function chartFillHeight(e,o,n,t){const i=z(o,n);return growCardToFitContent(e,o,i+t),Math.max(t,o.clientHeight-i)}export function chartDpr(e){return(e.ownerDocument.defaultView??window).devicePixelRatio||1}export function prepareCanvas(e,o,n,t){const i=chartDpr(e);e.width=Math.max(1,Math.floor(n*i)),e.height=Math.max(1,Math.floor(t*i)),e.style.width=`${n}px`,e.style.height=`${t}px`;const r=e.getContext("2d");return r?(r.setTransform(i,0,0,i,0,0),r.clearRect(0,0,n,t),r.font=`${k(-1)}px system-ui, sans-serif`,r.textBaseline="middle",{ctx:r,theme:resolveChartCanvasColors(o)}):null}export function applyCanvasFlow(e,o=0){let n;if(typeof o=="number")n=o;else{const t=(o.ownerDocument.defaultView??window).getComputedStyle(o);n=o.offsetTop+o.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}`:ne(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,o,n){if(!o&&n===void 0)return{tick:formatValue,tickWithUnit:formatValue,unitLabel:""};const{tickFmt:t,axisLabel:i,scaledUnit:r}=oe(e,null,o,"engineering",n);return{tick:t,tickWithUnit:r?l=>`${t(l)} ${r}`:t,unitLabel:i}}const ae=8;export const VERTICAL_TICK_SPACING_PX=28;export function horizontalTickSpacing(e,o,n){return(t,i)=>{const r=makeAxisFormat(o,n,t).tick;return i.reduce((l,s)=>Math.max(l,e.measureText(r(s)).width),0)+ae}}export function drawAxisUnit(e,o,n,t,i){const r={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font=`${k(-1)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="middle",e.fillStyle=i,e.fillText(`(${o})`,n,t),Object.assign(e,r)}export function saveCanvasPng(e,o,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const i=t.getContext("2d");i.fillStyle="#ffffff",i.fillRect(0,0,t.width,t.height),i.drawImage(e,0,0),t.toBlob(r=>{r&&J(r,o,n)},"image/png")}const V="10px";export function chartSwatchCss(e,o="solid"){return["display:inline-block",`width:${V}`,`height:${V}`,`border-radius:${C.pill}`,`background:${o==="outline"?"transparent":e}`,`border:1px ${o==="outline"?"dashed":"solid"} ${e}`,"box-sizing:border-box","flex-shrink:0"].join(";")}const ce="0.35";export function makeSeriesLegendItem(e,o,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:C.pill,border:`1px solid ${f.menuBorder}`,background:"none",cursor:"pointer",fontSize:S.body,color:f.text,whiteSpace:"nowrap"}),L(t,"bare");const i=n.createElement("span");return i.style.cssText=chartSwatchCss(o),t.append(i,n.createTextNode(e)),{el:t,setState({selected:r,dimmed:l}){t.style.borderColor=r?f.iconActive:f.menuBorder,t.style.fontWeight=r?"600":"400",t.style.opacity=l?ce:"1",t.setAttribute("aria-pressed",r?"true":"false")}}}export function cardFrameStyle(){return{background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:C.container,padding:m.xl,fontVariantNumeric:"tabular-nums"}}export function cardShell(e,o,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 i=t.ownerDocument.createElement("div");Object.assign(i.style,{display:"flex",alignItems:"center",gap:m.sm,paddingBottom:"6px",marginBottom:m.md,borderBottom:`1px solid ${f.menuBorder}`});const r=t.ownerDocument.createElement("div");r.textContent=e,Object.assign(r.style,{color:f.value,fontSize:S.heading,fontWeight:"600",flex:"1"}),i.appendChild(r);const l=makeTooltip(t),s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Save as PNG"),s.innerHTML=_.download,Object.assign(s.style,{...I("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:j.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(s),attachChartTip(s,t,l,"Save as PNG"),s.addEventListener("click",()=>{const u=t.querySelector("canvas");u&&saveCanvasPng(u,e,o)}),i.appendChild(s);const a=t.ownerDocument.createElement("button");a.type="button",a.setAttribute("aria-label","Expand"),a.innerHTML=_.expand,Object.assign(a.style,{...I("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:j.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(a),attachChartTip(a,t,l,"Expand"),a.dataset.wmapChartExpand="1",a.addEventListener("click",()=>openChartExpandModal(t,r.textContent??e)),i.appendChild(a),t.appendChild(i);const d=t.ownerDocument.createElement("div");Object.assign(d.style,{display:"flex",gap:m.sm,marginBottom:m.md,flexWrap:"wrap",alignItems:"center"}),t.appendChild(d);const c=t.ownerDocument.createElement("div");return Object.assign(c.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(c),{card:t,heading:r,controlsRow:d,body:c}}export function makeSegmented(e,o,n,t=document,i=!1){const r=t.createElement("div");r.setAttribute("role","radiogroup"),Object.assign(r.style,{display:"inline-flex",border:`1px solid ${f.menuBorder}`,borderRadius:C.control,overflow:"hidden"});const l=`seg-${Math.random().toString(36).slice(2,9)}`,s=[];return e.forEach(([a,d],c)=>{const u=t.createElement("label");Object.assign(u.style,{display:"inline-flex",alignItems:"center",fontSize:S.body,padding:i?"1px 6px":"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:c>0?`1px solid ${f.menuBorder}`:"none"}),L(u,"bare");const p=t.createElement("input");p.type="radio",p.name=l,p.value=a,p.checked=a===o,Object.assign(p.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const w=()=>{u.style.background=p.checked?f.bgActive:f.menuBg,u.style.color=p.checked?f.iconActive:f.text,u.dataset.on=p.checked?"true":"false"};s.push(w),w(),p.addEventListener("change",()=>{if(p.checked){for(const v of s)v();n(a)}}),u.append(p,t.createTextNode(d)),r.appendChild(u)}),r}export function makeBackButton(e,o=document){const n=o.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{...I("outlined"),background:f.menuBg,color:f.label}),L(n),n.addEventListener("click",e),n}export function renderEmptyState(e,o,n){const t=e.ownerDocument.createElement("div");t.textContent=o,Object.assign(t.style,{color:f.label,fontSize:S.body,padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeListSelect(e,o,n,t={}){const{maxWidth:i="200px",ownerDocument:r=document,ariaLabel:l,emptyText:s,searchPlaceholder:a="Filter\u2026",hook:d}=t;let c=o;const u=x=>e.find(O=>O.value===x)?.label??"",p=r.createElement("button");p.type="button",d&&(p.dataset.wmapSelect=d),l&&p.setAttribute("aria-label",l),Object.assign(p.style,{fontSize:S.body,padding:`${m.xxs} ${m.sm}`,background:f.menuBg,color:f.text,border:`1px solid ${f.menuBorder}`,borderRadius:C.control,maxWidth:i,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:m.sm,justifyContent:"space-between"}),L(p),R(p,!1);const w=r.createElement("span");Object.assign(w.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const v=r.createElement("span");v.textContent="\u25BE",v.style.flex="0 0 auto",p.append(w,v);const E=()=>{w.textContent=e.length===0?s??"(none)":u(c)||s||"(none)"};if(E(),e.length===0)return p.disabled=!0,p.style.opacity="0.6",p.style.cursor="default",Object.defineProperty(p,"value",{get:()=>c,set:x=>{c=x,E()}});let b=null;const B=()=>{b&&(b.remove(),b=null,R(p,!1))},$=()=>{B(),p.focus()};function G(){const x=p.getBoundingClientRect(),O=r.defaultView??window,N=Math.max(x.width,220),q=Math.min(x.left,Math.max(4,(O.innerWidth??1/0)-N-4));b=r.createElement("div"),b.setAttribute("role","listbox"),b.setAttribute("aria-label",l??"Options"),Object.assign(b.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${q}px`,background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:C.control,boxShadow:D.menu,zIndex:X,minWidth:`${N}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const T=[],F=()=>T.filter(h=>h.row.style.display!=="none").map(h=>h.row);if(e.length>W){const h=Z(y=>{for(const g of T)g.row.style.display=g.label.includes(y)?"":"none"},a,r);Object.assign(h.style,{position:"sticky",top:"0",zIndex:"1",background:f.menuBg}),h.addEventListener("keydown",y=>{if(y.key==="Escape"){$();return}y.key==="ArrowDown"&&(y.preventDefault(),F()[0]?.focus())}),b.appendChild(h)}for(const h of e){const y=h.value===c,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:S.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",U=>{U.stopPropagation(),c=h.value,E(),$(),n(h.value)}),b.appendChild(g),T.push({row:g,label:h.label.toLowerCase()})}if(Y(b,F,$),K(p).appendChild(b),R(p,!0),e.length<=W){const h=e.findIndex(y=>y.value===c);F()[h>=0?h:0]?.focus()}}p.addEventListener("click",x=>{if(x.stopPropagation(),b){B();return}G()});const H=x=>{if(!p.isConnected){r.removeEventListener("click",H);return}b&&!b.contains(x.target)&&!p.contains(x.target)&&B()};return r.addEventListener("click",H),Object.defineProperty(p,"value",{get:()=>c,set:x=>{c=x,E()}})}export function testOptionLabels(e){const o=e.some(n=>n.derived);return e.map(n=>(o&&!n.derived?re:"")+ie(n,n.testNumber))}export function makeTestSelect(e,o,n,t={}){const{maxWidth:i="200px",emptyText:r="No parametric tests",ownerDocument:l=document}=t;return makeListSelect(testOptionLabels(e).map((s,a)=>({value:String(e[a].testNumber),label:s})),o!==null?String(o):"",s=>n(Number(s)),{maxWidth:i,ownerDocument:l,ariaLabel:"Test",emptyText:r,searchPlaceholder:"Filter tests\u2026"})}export function makeLinkedAxisPrefs(e,o,n,t=document){let i=o?.includeLimits,r=o?.clipOutliers??!1,l=o?.limits??"both";const s=()=>n({includeLimits:i,clipOutliers:r,limits:l});return{get:()=>({includeLimits:i,clipOutliers:r,limits:l}),sync(a,d,c=!1){e.innerHTML="",c&&e.appendChild(makeLimitsSelect(l,u=>{l=u,s()},t)),d&&e.appendChild(makeToggle("Axis includes limits",a,u=>{i=u,s()},t)),e.appendChild(makeToggle("Clip outliers",r,u=>{r=u,s()},t))},set(a){const d=a.limits??"both";return a.includeLimits===i&&a.clipOutliers===r&&d===l?!1:(i=a.includeLimits,r=a.clipOutliers,l=d,!0)}}}export function makeLinkedTestSelect(e,o,n,t={}){let i=o;const r=makeTestSelect(e,i,l=>{l!==i&&(i=l,n(l))},t);return{el:r,get:()=>i,set(l){return i===l||!e.some(s=>s.testNumber===l)?!1:(i=l,r.value=String(l),!0)}}}export const ALL_GROUPS="\0all";export function makeLinkedGroupSelect(e,o,n,t,i={}){let r=n;const l=c=>c??ALL_GROUPS,s=c=>c===ALL_GROUPS?null:c,a=makeLabeledSelect(o,[{value:ALL_GROUPS,label:"All groups"},...e.map(c=>({value:c,label:c}))],l(r),c=>{const u=s(c);u!==r&&(r=u,t(u))},{...i,hook:"group-scope"}),d=a.querySelector('[data-wmap-select="group-scope"]');return{el:a,get:()=>r,set(c){return r===c||c!==null&&!e.includes(c)?!1:(r=c,d&&(d.value=l(c)),!0)}}}export function makeWaferSelect(e,o,n,t={}){const{maxWidth:i="160px",allLabel:r="All wafers",ownerDocument:l=document}=t,s="\0all";return makeListSelect([{value:s,label:r},...e.map((a,d)=>({value:String(d),label:a.label??`#${d}`}))],o===null?s:String(o),a=>n(a===s?null:Number(a)),{maxWidth:i,ownerDocument:l,ariaLabel:r,searchPlaceholder:"Filter wafers\u2026"})}export function makeToggle(e,o,n,t=document){const i=t.createElement("label");Object.assign(i.style,{display:"inline-flex",alignItems:"center",gap:m.xs,fontSize:S.body,color:f.label,cursor:"pointer",userSelect:"none"}),L(i,"bare");const r=t.createElement("input");return r.type="checkbox",r.checked=o,r.style.cssText="margin:0;cursor:pointer;",r.addEventListener("change",()=>n(r.checked)),i.append(r,t.createTextNode(e)),i}export function makeLabeledSelect(e,o,n,t,i={}){const{maxWidth:r="160px",hook:l,ownerDocument:s=document}=i,a=s.createElement("label");a.textContent=e,Object.assign(a.style,{color:f.label,fontSize:S.body,display:"flex",alignItems:"center",gap:m.xs});const d=makeListSelect(o,n,t,{maxWidth:r,ownerDocument:s,hook:l,ariaLabel:e.replace(/:\s*$/,"")});return a.appendChild(d),a}export function makeChartGridWrap(e=document){const o=e.createElement("div");return o.dataset.wmapChartGrid="1",Object.assign(o.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:m.lg,flex:"0 0 auto"}),o}export function makeTooltip(e){const o=e.ownerDocument.createElement("div");return Object.assign(o.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:C.control,padding:`${m.sm} ${m.xl}`,fontSize:S.sub,fontFamily:S.family,maxWidth:"320px",whiteSpace:"normal",overflowWrap:"break-word",boxShadow:D.menu}),e.appendChild(o),o}export function attachChartTip(e,o,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",i=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,o,i.clientX,i.clientY)}),e.addEventListener("mousemove",i=>{n.style.display==="block"&&positionChartTooltip(n,o,i.clientX,i.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 i=e.getBoundingClientRect();positionChartTooltip(n,o,i.left+i.width/2,i.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,o,n,t){const i=o.getBoundingClientRect(),r=14,l=4;e.style.left="0",e.style.top="0";const s=e.offsetWidth,a=e.offsetHeight;let d=n-i.left+r,c=t-i.top+r;d+s+l>i.width&&(d=n-i.left-r-s),c+a+l>i.height&&(c=t-i.top-r-a),d=Math.max(l,d),c=Math.max(l,c),e.style.left=`${d}px`,e.style.top=`${c}px`}export const LIMIT_LABEL={test:{lo:"Lo limit",hi:"Hi limit"},spec:{lo:"LSL",hi:"USL"}},LIMIT_DASH={test:[3,3],spec:[10,4]},LIMITS_SHOWN_OPTIONS=[{value:"both",label:"Test + spec"},{value:"test",label:"Test limits"},{value:"spec",label:"Spec limits (LSL/USL)"},{value:"none",label:"None"}];function A(e,o){const[n,t]=o==="test"?[e?.limitLow,e?.limitHigh]:[e?.specLow,e?.specHigh],i=[];return n!==void 0&&Number.isFinite(n)&&i.push({value:n,kind:o,end:"lo",label:LIMIT_LABEL[o].lo}),t!==void 0&&Number.isFinite(t)&&i.push({value:t,kind:o,end:"hi",label:LIMIT_LABEL[o].hi}),i}export function hasBothLimitKinds(e){return A(e,"test").length>0&&A(e,"spec").length>0}export function limitLines(e,o="both"){if(o==="none")return[];const n=A(e,"test"),t=A(e,"spec"),i=o==="test"?n:o==="spec"?t:[...n,...t];return i.length?i:[...n,...t]}export function limitExtent(e){if(!e.length)return{};const o=e.map(n=>n.value);return{lo:ee(o),hi:te(o)}}export function stackLabelRows(e,o=4){const n=e.map((r,l)=>({...r,i:l})).sort((r,l)=>r.start-l.start),t=[],i=new Array(e.length).fill(0);for(const r of n){let l=t.findIndex(s=>r.start>=s+o);l===-1?(l=t.length,t.push(r.end)):t[l]=r.end,i[r.i]=l}return i}export function fillTextOnHalo(e,o,n,t,i){if(i){const r=e.measureText(o),l=2,s=e.fillStyle;e.fillStyle=i,e.fillRect(n-r.actualBoundingBoxLeft-l,t-r.actualBoundingBoxAscent-l,r.actualBoundingBoxLeft+r.actualBoundingBoxRight+2*l,r.actualBoundingBoxAscent+r.actualBoundingBoxDescent+2*l),e.fillStyle=s}e.fillText(o,n,t)}export function strokeLimitLine(e,o,n,t,i,r,l){e.save(),e.strokeStyle=n,e.lineWidth=1,e.setLineDash(LIMIT_DASH[o.kind]),e.beginPath(),e.moveTo(t,i),e.lineTo(r,l),e.stroke(),e.restore()}export function makeLimitsSelect(e,o,n=document){return makeLabeledSelect("Limits:",LIMITS_SHOWN_OPTIONS,e,t=>o(t),{maxWidth:"170px",hook:"limits-shown",ownerDocument:n})}export function robustFence(e,o=1.5){const n=Float64Array.from(e.filter(s=>Number.isFinite(s)));if(n.sort(),n.length<8)return null;const t=s=>{const a=(n.length-1)*s,d=Math.floor(a),c=Math.ceil(a);return d===c?n[d]:n[d]+(n[c]-n[d])*(a-d)},i=t(.25),r=t(.75),l=r-i;return l===0?null:{lo:i-o*l,hi:r+o*l}}export function shouldIncludeLimitsByDefault(e,o,n,t,i=1/3){if(n===void 0&&t===void 0||!Number.isFinite(e)||!Number.isFinite(o))return!1;const r=Math.min(e,n??e),s=Math.max(o,t??o)-r;if(s<=0)return!0;const a=o-e;return a===0?!0:a/s>=i}export function resolveAxisRange(e){const{dataMin:o,dataMax:n,limits:t=[],includeLimits:i,clipOutliers:r,values:l}=e;let s=o,a=n,d=0;if(r&&l?.length){const u=robustFence(l);if(u){const p=Math.max(s,u.lo),w=Math.min(a,u.hi);w>p&&(d=l.filter(v=>Number.isFinite(v)&&(v<p||v>w)).length,s=p,a=w)}}if(i)for(const u of t)s=Math.min(s,u.value),a=Math.max(a,u.value);s===a&&(s-=1,a+=1);const c=[];for(const u of t)u.value<s?c.push({value:u.value,label:u.label,side:"lo"}):u.value>a&&c.push({value:u.value,label:u.label,side:"hi"});return{lo:s,hi:a,offAxis:c,clippedCount:d}}export function limitLabelSide(e,o,n,t,i,r=3){const l=i?-1:1;return(l<0?e-r-o>=n:e+r+o<=t)?l:-l}export function drawOffAxisLimits(e,o,n,t,i,r,l){if(!o.length)return;e.save(),e.fillStyle=i,e.font=`${k(-1)}px system-ui, sans-serif`;const s={lo:0,hi:0},a=k(-1)+2;for(const{value:d,label:c,side:u}of o){const p=s[u]++*a,w=t==="horizontal"?u==="lo"?"\u2190":"\u2192":u==="lo"?"\u2193":"\u2191",v=u==="lo"?`${w} ${c} ${r(d)}`:`${c} ${r(d)} ${w}`;t==="horizontal"?(e.textBaseline="top",e.textAlign=u==="lo"?"left":"right",fillTextOnHalo(e,v,u==="lo"?n.left+2:n.right-2,n.top+2+p,l)):(e.textBaseline=u==="lo"?"bottom":"top",e.textAlign="left",fillTextOnHalo(e,v,n.left+4,u==="lo"?n.bottom-2-p:n.top+2+p,l))}e.restore()}
@@ -1 +1 @@
1
- import{buildTestHistogramData as Xt,collectTestValues as Ht,buildTestHistogramSeries as Yt,testValueExtent as At}from"../../stats/histogram.js";import{SPACE as st,fontPx as Y,FONT as Ct,CLR as $t}from"../toolbar.js";import{fmt as pt}from"../../renderer/fmt.js";import{niceStep as Qt,fitTicks as Wt}from"../../renderer/axisTicks.js";import{QUANTITY as Ut,categorical as qt}from"./palette.js";import{cardShell as Jt,observeResize as Dt,makeTooltip as Zt,attachChartTip as te,positionChartTooltip as Et,makeLinkedTestSelect as ee,makeWaferSelect as ne,makeLinkedAxisPrefs as ie,renderEmptyState as Tt,chartFillHeight as Pt,applyCanvasFlow as It,makeAxisFormat as vt,horizontalTickSpacing as Bt,PADDING as ut,robustFence as Rt,shouldIncludeLimitsByDefault as Ot,drawOffAxisLimits as oe,resolveAxisRange as le,chartSwatchCss as se,makeSeriesLegendItem as ae,prepareCanvas as Ft}from"./chartShell.js";import{escHtml as Mt,maxOf as re}from"../../core/utils.js";const Lt=230,Kt=36,xt=18;function Nt(o,z,G,Q,bt,R,r,y=4){const at=Math.max(1,Qt(R/y)),mt=Q-G;o.save(),o.font=`${Y(-1)}px system-ui, sans-serif`,o.textAlign="right",o.textBaseline="middle";for(let c=0;c<=R+1e-9;c+=at){const F=Q-c/(R||1)*mt;o.strokeStyle=r.grid,o.lineWidth=1,o.beginPath(),o.moveTo(z,F),o.lineTo(z+bt,F),o.stroke(),o.fillStyle=r.text,o.fillText(String(Math.round(c)),z-4,F)}o.strokeStyle=r.axis,o.lineWidth=1,o.beginPath(),o.moveTo(z,G),o.lineTo(z,Q),o.stroke(),o.restore()}export function renderHistogramPanel(o){const{title:z="Test value distribution",items:G,testDefs:Q,onSaveImage:bt,groups:R}=o,{card:r,body:y,controlsRow:at}=Jt(z,bt,o.ownerDocument),mt=Q.filter(l=>l.testNumber!==void 0);let c=o.selectedTestNumber??mt[0]?.testNumber??null,F=null,U=0;const kt=ee(mt,c,l=>{c=l,V(),o.onTestChange?.(l)},{maxWidth:"200px",ownerDocument:r.ownerDocument}),zt=kt.el;at.appendChild(zt);const ft=G.length>1?ne(G,F,l=>{F=l,V()},{ownerDocument:r.ownerDocument}):null;ft&&at.appendChild(ft);function Gt(l){return M===l||l!==null&&!(R??[]).some(k=>k.key===l)?!1:(M=l,V(),!0)}const yt=r.ownerDocument.createElement("span");Object.assign(yt.style,{display:"inline-flex",gap:st.lg,alignItems:"center"}),at.appendChild(yt);const ht=ie(yt,o.axisPrefs,l=>{o.onAxisPrefsChange?.(l),V()},r.ownerDocument),C=Zt(r);let rt=null,M=null;function q(l){const k=Q.find(J=>J.testNumber===l);return{unit:k?.unit,limitLow:k?.limitLow,limitHigh:k?.limitHigh}}function V(){const{includeLimits:l,clipOutliers:k}=ht.get();if(rt?.disconnect(),rt=null,y.innerHTML="",c===null){Tt(y,"No parametric test data available for a histogram.");return}const J=!!R&&R.length>0;let Z=!1,S;if(J){const{limitLow:s,limitHigh:u}=q(c),i=R.flatMap(b=>b.items),{min:e,max:x}=At(i,c);if(Z=l??Ot(e,x,s,u),k){const b=Ht(i,c),L=Rt(b);L&&(S={lo:Math.max(e,L.lo),hi:Math.min(x,L.hi)},U=b.reduce((it,A)=>it+(A<S.lo||A>S.hi?1:0),0))}S||(U=0)}const $=J?Yt(R,c,16,Z?q(c).limitLow:void 0,Z?q(c).limitHigh:void 0,S):null;if(ft&&(ft.style.display=$?"none":""),$){const{limitLow:s,limitHigh:u}=q(c);if(ht.sync(Z,s!==void 0||u!==void 0),$.series.length===0){Tt(y,"No parametric test data available for a histogram.");return}Vt($);return}const j=F!==null?[G[F]]:G,{unit:W,limitLow:D,limitHigh:E}=q(c),w=Ht(j,c),{min:K,max:tt}=At(j,c),et=l??Ot(K,tt,D,E);ht.sync(et,D!==void 0||E!==void 0);const nt=k?Rt(w):null,_=nt?{lo:Math.max(K,nt.lo),hi:Math.min(tt,nt.hi)}:void 0;U=_?w.filter(s=>s<_.lo||s>_.hi).length:0;const H=Xt(j,c,16,et?D:void 0,et?E:void 0,_);if(H.length===0){Tt(y,"No parametric test data available for a histogram.");return}const P=Math.max(re(H.map(s=>s.count)),1),X=r.ownerDocument.createElement("div");Object.assign(X.style,{fontSize:Ct.body,color:$t.label,marginBottom:st.xxs}),X.textContent=`max ${P} dies/bucket`+(U?` \xB7 ${U} value${U===1?"":"s"} outside clipped range`:""),y.appendChild(X);const T=r.ownerDocument.createElement("canvas");T.style.display="block",T.style.cursor="default",y.appendChild(T);let O=-1;const n=H[0].rangeLow,f=H[H.length-1].rangeHigh,g=f-n||1,t=Math.max(Math.abs(n),Math.abs(f));function d(s){const u=ut+36,i=T.clientWidth-u-ut,e=s-Kt-xt;return{plotX:u,plotMaxWidth:Math.max(10,i),plotMaxHeight:e,plotTop:xt}}function h(s){const{plotX:u,plotMaxWidth:i}=d(Lt);if(s<u||s>u+i)return-1;const e=Math.floor((s-u)/i*H.length);return e>=0&&e<H.length?e:-1}function p(){It(T,X);const s=y.clientWidth,u=Pt(r,y,T,Lt),i=Ft(T,r,s,u);if(!i)return;const{ctx:e,theme:x}=i,{plotX:b,plotMaxWidth:L,plotMaxHeight:it,plotTop:A}=d(u),a=A+it,v=L/H.length;Nt(e,b,A,a,L,P,{text:x.textMuted,axis:x.border,grid:x.border}),H.forEach((m,B)=>{const lt=m.count/P*it,St=b+B*v,wt=a-lt;e.fillStyle=B===O?x.text:Ut,e.globalAlpha=B===O?1:.7,e.fillRect(St+1,wt,Math.max(1,v-2),lt),e.globalAlpha=1,B===O&&(e.fillStyle=x.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${m.count}`,St+v/2,wt-2))});const N=m=>b+(m-n)/g*L;e.font=`${Y(-1)}px system-ui, sans-serif`;const{offAxis:ct}=le({dataMin:n,dataMax:f,limitLow:D,limitHigh:E,includeLimits:!1});oe(e,ct,{left:b,right:b+L,top:A,bottom:a},"horizontal",x.limitLine,vt(t,W).tick);const gt=new Set(ct.map(m=>m.value));for(const[m,B]of[[D,"Lo limit"],[E,"Hi limit"]]){if(m===void 0||gt.has(m))continue;const lt=N(m);e.strokeStyle=x.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(lt,A),e.lineTo(lt,a),e.stroke(),e.setLineDash([]),e.fillStyle=x.limitLine,e.textAlign="center",e.textBaseline="bottom",e.fillText(B,lt,A-2)}e.font=`${Y(-1)}px system-ui, sans-serif`,e.strokeStyle=x.border,e.lineWidth=1,e.beginPath(),e.moveTo(b,a),e.lineTo(b+L,a),e.stroke(),e.fillStyle=x.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:dt,step:ot}=Wt(n,f,L,Bt(e,t,W)),I=vt(t,W,ot||void 0);for(const m of dt){const B=b+(m-n)/g*L;e.beginPath(),e.moveTo(B,a),e.lineTo(B,a+4),e.stroke(),e.fillText(I.tick(m),B,a+6)}I.unitLabel&&(e.save(),e.font=`${Y(-1)}px system-ui, sans-serif`,e.fillStyle=x.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${I.unitLabel})`,b+L,a+20),e.restore())}T.addEventListener("mousemove",s=>{const u=T.getBoundingClientRect(),i=h(s.clientX-u.left);if(i!==O&&(O=i,p()),i>=0){const e=H[i];C.innerHTML=`<strong>${Mt(`${pt(e.rangeLow,W,"engineering")} \u2013 ${pt(e.rangeHigh,W,"engineering")}`)}</strong><br>${e.count} dies`,C.style.display="block",Et(C,r,s.clientX,s.clientY)}else C.style.display="none"}),T.addEventListener("mouseleave",()=>{O!==-1&&(O=-1,p()),C.style.display="none"}),rt=Dt(r,()=>p()),p()}function Vt(l){const{unit:k,limitLow:J,limitHigh:Z}=q(c),S=l.ranges,$=l.series,j=qt;M&&!$.some(n=>n.groupKey===M)&&(M=null);const W=Math.max(1,...$.flatMap(n=>n.counts)),D=r.ownerDocument.createElement("div");Object.assign(D.style,{fontSize:Ct.body,color:$t.label,marginBottom:st.xxs}),D.textContent=`${$.length} groups \xB7 max ${W} dies/bucket`,y.appendChild(D);const E=r.ownerDocument.createElement("div");Object.assign(E.style,{display:"flex",flexWrap:"wrap",gap:st.sm,marginTop:st.md,marginBottom:st.xl}),$.forEach((n,f)=>{const g=ae(n.groupKey,j(f),r.ownerDocument),t=g.el;te(t,r,C,`${n.groupKey} \u2014 click to emphasize (dim the rest)`),g.setState({selected:M===n.groupKey,dimmed:M!==null&&M!==n.groupKey}),t.addEventListener("click",()=>{const d=M===n.groupKey?null:n.groupKey;Gt(d),o.onGroupChange?.(d)}),E.appendChild(t)}),y.appendChild(E);const w=r.ownerDocument.createElement("canvas");w.style.display="block",w.style.cursor="default",y.appendChild(w);const K=S[0].rangeLow,tt=S[S.length-1].rangeHigh,et=tt-K||1,nt=Math.max(Math.abs(K),Math.abs(tt)),_=n=>{const f=(n.ownerDocument.defaultView??window).getComputedStyle(n);return n.offsetHeight+(parseFloat(f.marginTop)||0)+(parseFloat(f.marginBottom)||0)},H=()=>_(D)+_(E);let P=-1,X={plotX:ut+36,plotMaxWidth:10,barWidth:10};function T(){It(w,H());const n=y.clientWidth,f=Pt(r,y,w,Lt),g=Ft(w,r,n,f);if(!g)return;const{ctx:t,theme:d}=g,h=ut+36,p=Math.max(10,n-h-ut),s=xt,u=f-Kt-xt,i=s+u,e=p/S.length;X={plotX:h,plotMaxWidth:p,barWidth:e},Nt(t,h,s,i,p,W,{text:d.textMuted,axis:d.border,grid:d.border}),P>=0&&(t.fillStyle=d.bgHover,t.globalAlpha=.5,t.fillRect(h+P*e,s,e,u),t.globalAlpha=1);const x=$.map((a,v)=>({s:a,i:v})).sort((a,v)=>(a.s.groupKey===M?1:0)-(v.s.groupKey===M?1:0));for(const{s:a,i:v}of x){const N=M===a.groupKey,ct=M!==null&&!N,gt=j(v);t.strokeStyle=gt,t.fillStyle=gt,t.lineWidth=N?2:1,t.globalAlpha=ct?.12:M?.32:.22,t.beginPath(),t.moveTo(h,i),a.counts.forEach((dt,ot)=>{const I=h+ot*e,m=i-dt/W*u;t.lineTo(I,m),t.lineTo(I+e,m)}),t.lineTo(h+p,i),t.closePath(),t.fill(),t.globalAlpha=ct?.3:1,t.beginPath(),a.counts.forEach((dt,ot)=>{const I=h+ot*e,m=i-dt/W*u;ot===0?t.moveTo(I,m):t.lineTo(I,m),t.lineTo(I+e,m)}),t.stroke()}t.globalAlpha=1;const b=a=>h+(a-K)/et*p;t.font=`${Y(-1)}px system-ui, sans-serif`;for(const[a,v]of[[J,"Lo limit"],[Z,"Hi limit"]]){if(a===void 0)continue;const N=b(a);t.strokeStyle=d.limitLine,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(N,s),t.lineTo(N,i),t.stroke(),t.setLineDash([]),t.fillStyle=d.limitLine,t.textAlign="center",t.textBaseline="bottom",t.fillText(v,N,s-2)}t.font=`${Y(-1)}px system-ui, sans-serif`,t.strokeStyle=d.border,t.lineWidth=1,t.beginPath(),t.moveTo(h,i),t.lineTo(h+p,i),t.stroke(),t.fillStyle=d.textMuted,t.textAlign="center",t.textBaseline="top";const{ticks:L,step:it}=Wt(K,tt,p,Bt(t,nt,k)),A=vt(nt,k,it||void 0);for(const a of L){const v=h+(a-K)/et*p;t.beginPath(),t.moveTo(v,i),t.lineTo(v,i+4),t.stroke(),t.fillText(A.tick(a),v,i+6)}A.unitLabel&&(t.save(),t.font=`${Y(-1)}px system-ui, sans-serif`,t.fillStyle=d.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${A.unitLabel})`,h+p,i+20),t.restore())}function O(n){const{plotX:f,plotMaxWidth:g,barWidth:t}=X;if(n<f||n>f+g)return-1;const d=Math.floor((n-f)/t);return d>=0&&d<S.length?d:-1}w.addEventListener("mousemove",n=>{const f=w.getBoundingClientRect(),g=O(n.clientX-f.left);if(g!==P&&(P=g,T()),g>=0){const t=S[g],d=$.map((h,p)=>`<span style="${se(j(p))};margin-right:4px"></span>${Mt(`${h.groupKey}: ${h.counts[g]}`)}`).join("<br>");C.innerHTML=`<strong>${Mt(`${pt(t.rangeLow,k,"engineering")} \u2013 ${pt(t.rangeHigh,k,"engineering")}`)}</strong><br>${d}`,C.style.display="block",Et(C,r,n.clientX,n.clientY)}else C.style.display="none"}),w.addEventListener("mouseleave",()=>{P!==-1&&(P=-1,T()),C.style.display="none"}),rt=Dt(r,()=>T()),T()}V();function jt(l){kt.set(l)&&(c=l,V())}function _t(l){ht.set(l)&&V()}return{card:r,setTest:jt,setAxisPrefs:_t,destroy:()=>rt?.disconnect()}}
1
+ import{buildTestHistogramData as qt,collectTestValues as Ht,buildTestHistogramSeries as Jt,testValueExtent as At}from"../../stats/histogram.js";import{SPACE as lt,fontPx as R,FONT as Ct,CLR as $t}from"../toolbar.js";import{fmt as gt}from"../../renderer/fmt.js";import{niceStep as Zt,fitTicks as Et}from"../../renderer/axisTicks.js";import{QUANTITY as te,categorical as ee}from"./palette.js";import{cardShell as ne,observeResize as Wt,makeTooltip as ie,attachChartTip as oe,positionChartTooltip as Bt,makeLinkedTestSelect as se,makeWaferSelect as le,makeLinkedAxisPrefs as ae,renderEmptyState as yt,chartFillHeight as Dt,applyCanvasFlow as It,makeAxisFormat as Tt,horizontalTickSpacing as Pt,PADDING as mt,robustFence as Kt,shouldIncludeLimitsByDefault as Rt,drawOffAxisLimits as re,resolveAxisRange as ce,limitLines as Ot,limitExtent as de,hasBothLimitKinds as ue,stackLabelRows as me,strokeLimitLine as fe,chartSwatchCss as he,makeSeriesLegendItem as pe,prepareCanvas as Ft}from"./chartShell.js";import{escHtml as Mt,maxOf as ge}from"../../core/utils.js";const vt=230,Nt=36,xe=18;function zt(n){const C=new Set(n.map($=>$.kind)).size;return xe+Math.max(0,C-1)*(R(-1)+2)}function Gt(n,C,$,G,et,E,s){n.font=`${R(-1)}px system-ui, sans-serif`;const h=C.filter(i=>!$.has(i.value)).map(i=>{const b=G(i.value),W=n.measureText(i.label).width;return{l:i,x:b,start:b-W/2,end:b+W/2}}),U=me(h),nt=R(-1)+2;h.forEach(({l:i,x:b},W)=>{fe(n,i,s,b,et,b,E),n.fillStyle=s,n.textAlign="center",n.textBaseline="bottom",n.fillText(i.label,b,et-2-U[W]*nt)})}function Vt(n,C,$,G,et,E,s,h=4){const U=Math.max(1,Zt(E/h)),nt=G-$;n.save(),n.font=`${R(-1)}px system-ui, sans-serif`,n.textAlign="right",n.textBaseline="middle";for(let i=0;i<=E+1e-9;i+=U){const b=G-i/(E||1)*nt;n.strokeStyle=s.grid,n.lineWidth=1,n.beginPath(),n.moveTo(C,b),n.lineTo(C+et,b),n.stroke(),n.fillStyle=s.text,n.fillText(String(Math.round(i)),C-4,b)}n.strokeStyle=s.axis,n.lineWidth=1,n.beginPath(),n.moveTo(C,$),n.lineTo(C,G),n.stroke(),n.restore()}export function renderHistogramPanel(n){const{title:C="Test value distribution",items:$,testDefs:G,onSaveImage:et,groups:E}=n,{card:s,body:h,controlsRow:U}=ne(C,et,n.ownerDocument),nt=G.filter(a=>a.testNumber!==void 0);let i=n.selectedTestNumber??nt[0]?.testNumber??null,b=null,W=0;const kt=se(nt,i,a=>{i=a,q(),n.onTestChange?.(a)},{maxWidth:"200px",ownerDocument:s.ownerDocument}),jt=kt.el;U.appendChild(jt);const ft=$.length>1?le($,b,a=>{b=a,q()},{ownerDocument:s.ownerDocument}):null;ft&&U.appendChild(ft);function _t(a){return y===a||a!==null&&!(E??[]).some(T=>T.key===a)?!1:(y=a,q(),!0)}const xt=s.ownerDocument.createElement("span");Object.assign(xt.style,{display:"inline-flex",gap:lt.lg,alignItems:"center"}),U.appendChild(xt);const at=ae(xt,n.axisPrefs,a=>{n.onAxisPrefsChange?.(a),q()},s.ownerDocument),B=ie(s);let rt=null,y=null;function it(a){const T=G.find(D=>D.testNumber===a),V=Ot(T,at.get().limits),{lo:w,hi:M}=de(V);return{unit:T?.unit,lines:V,limitLow:w,limitHigh:M,hasLimits:Ot(T).length>0,hasBothKinds:ue(T)}}function q(){const{includeLimits:a,clipOutliers:T}=at.get();if(rt?.disconnect(),rt=null,h.innerHTML="",i===null){yt(h,"No parametric test data available for a histogram.");return}const V=!!E&&E.length>0;let w=!1,M;if(V){const{limitLow:d,limitHigh:g}=it(i),x=E.flatMap(l=>l.items),{min:e,max:f}=At(x,i);if(w=a??Rt(e,f,d,g),T){const l=Ht(x,i),u=Kt(l);u&&(M={lo:Math.max(e,u.lo),hi:Math.min(f,u.hi)},W=l.reduce((Z,K)=>Z+(K<M.lo||K>M.hi?1:0),0))}M||(W=0)}const D=V?Jt(E,i,16,w?it(i).limitLow:void 0,w?it(i).limitHigh:void 0,M):null;if(ft&&(ft.style.display=D?"none":""),D){const{hasLimits:d,hasBothKinds:g}=it(i);if(at.sync(w,d,g),D.series.length===0){yt(h,"No parametric test data available for a histogram.");return}Xt(D);return}const j=b!==null?[$[b]]:$,{unit:I,lines:_,limitLow:S,limitHigh:X,hasLimits:ct,hasBothKinds:ht}=it(i),dt=Ht(j,i),{min:ut,max:pt}=At(j,i),P=a??Rt(ut,pt,S,X);at.sync(P,ct,ht);const ot=T?Kt(dt):null,O=ot?{lo:Math.max(ut,ot.lo),hi:Math.min(pt,ot.hi)}:void 0;W=O?dt.filter(d=>d<O.lo||d>O.hi).length:0;const H=qt(j,i,16,P?S:void 0,P?X:void 0,O);if(H.length===0){yt(h,"No parametric test data available for a histogram.");return}const o=Math.max(ge(H.map(d=>d.count)),1),p=s.ownerDocument.createElement("div");Object.assign(p.style,{fontSize:Ct.body,color:$t.label,marginBottom:lt.xxs}),p.textContent=`max ${o} dies/bucket`+(W?` \xB7 ${W} value${W===1?"":"s"} outside clipped range`:""),h.appendChild(p);const r=s.ownerDocument.createElement("canvas");r.style.display="block",r.style.cursor="default",h.appendChild(r);let t=-1;const c=H[0].rangeLow,m=H[H.length-1].rangeHigh,v=m-c||1,F=Math.max(Math.abs(c),Math.abs(m));function J(d){const g=mt+36,x=r.clientWidth-g-mt,e=zt(_),f=d-Nt-e;return{plotX:g,plotMaxWidth:Math.max(10,x),plotMaxHeight:f,plotTop:e}}function k(d){const{plotX:g,plotMaxWidth:x}=J(vt);if(d<g||d>g+x)return-1;const e=Math.floor((d-g)/x*H.length);return e>=0&&e<H.length?e:-1}function A(){It(r,p);const d=h.clientWidth,g=Dt(s,h,r,vt),x=Ft(r,s,d,g);if(!x)return;const{ctx:e,theme:f}=x,{plotX:l,plotMaxWidth:u,plotMaxHeight:Z,plotTop:K}=J(g),L=K+Z,tt=u/H.length;Vt(e,l,K,L,u,o,{text:f.textMuted,axis:f.border,grid:f.grid}),H.forEach((z,Q)=>{const wt=z.count/o*Z,Lt=l+Q*tt,St=L-wt;e.fillStyle=Q===t?f.text:te,e.globalAlpha=Q===t?1:.7,e.fillRect(Lt+1,St,Math.max(1,tt-2),wt),e.globalAlpha=1,Q===t&&(e.fillStyle=f.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${z.count}`,Lt+tt/2,St-2))});const st=z=>l+(z-c)/v*u;e.font=`${R(-1)}px system-ui, sans-serif`;const{offAxis:N}=ce({dataMin:c,dataMax:m,limits:_,includeLimits:!1});re(e,N,{left:l,right:l+u,top:K,bottom:L},"horizontal",f.limitLine,Tt(F,I).tick),Gt(e,_,new Set(N.map(z=>z.value)),st,K,L,f.limitLine),e.font=`${R(-1)}px system-ui, sans-serif`,e.strokeStyle=f.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,L),e.lineTo(l+u,L),e.stroke(),e.fillStyle=f.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:Y,step:Ut}=Et(c,m,u,Pt(e,F,I)),bt=Tt(F,I,Ut||void 0);for(const z of Y){const Q=l+(z-c)/v*u;e.beginPath(),e.moveTo(Q,L),e.lineTo(Q,L+4),e.stroke(),e.fillText(bt.tick(z),Q,L+6)}bt.unitLabel&&(e.save(),e.font=`${R(-1)}px system-ui, sans-serif`,e.fillStyle=f.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${bt.unitLabel})`,l+u,L+20),e.restore())}r.addEventListener("mousemove",d=>{const g=r.getBoundingClientRect(),x=k(d.clientX-g.left);if(x!==t&&(t=x,A()),x>=0){const e=H[x];B.innerHTML=`<strong>${Mt(`${gt(e.rangeLow,I,"engineering")} \u2013 ${gt(e.rangeHigh,I,"engineering")}`)}</strong><br>${e.count} dies`,B.style.display="block",Bt(B,s,d.clientX,d.clientY)}else B.style.display="none"}),r.addEventListener("mouseleave",()=>{t!==-1&&(t=-1,A()),B.style.display="none"}),rt=Wt(s,()=>A()),A()}function Xt(a){const{unit:T,lines:V}=it(i),w=a.ranges,M=a.series,D=ee;y&&!M.some(o=>o.groupKey===y)&&(y=null);const j=Math.max(1,...M.flatMap(o=>o.counts)),I=s.ownerDocument.createElement("div");Object.assign(I.style,{fontSize:Ct.body,color:$t.label,marginBottom:lt.xxs}),I.textContent=`${M.length} groups \xB7 max ${j} dies/bucket`,h.appendChild(I);const _=s.ownerDocument.createElement("div");Object.assign(_.style,{display:"flex",flexWrap:"wrap",gap:lt.sm,marginTop:lt.md,marginBottom:lt.xl}),M.forEach((o,p)=>{const r=pe(o.groupKey,D(p),s.ownerDocument),t=r.el;oe(t,s,B,`${o.groupKey} \u2014 click to emphasize (dim the rest)`),r.setState({selected:y===o.groupKey,dimmed:y!==null&&y!==o.groupKey}),t.addEventListener("click",()=>{const c=y===o.groupKey?null:o.groupKey;_t(c),n.onGroupChange?.(c)}),_.appendChild(t)}),h.appendChild(_);const S=s.ownerDocument.createElement("canvas");S.style.display="block",S.style.cursor="default",h.appendChild(S);const X=w[0].rangeLow,ct=w[w.length-1].rangeHigh,ht=ct-X||1,dt=Math.max(Math.abs(X),Math.abs(ct)),ut=o=>{const p=(o.ownerDocument.defaultView??window).getComputedStyle(o);return o.offsetHeight+(parseFloat(p.marginTop)||0)+(parseFloat(p.marginBottom)||0)},pt=()=>ut(I)+ut(_);let P=-1,ot={plotX:mt+36,plotMaxWidth:10,barWidth:10};function O(){It(S,pt());const o=h.clientWidth,p=Dt(s,h,S,vt),r=Ft(S,s,o,p);if(!r)return;const{ctx:t,theme:c}=r,m=mt+36,v=Math.max(10,o-m-mt),F=zt(V),J=p-Nt-F,k=F+J,A=v/w.length;ot={plotX:m,plotMaxWidth:v,barWidth:A},Vt(t,m,F,k,v,j,{text:c.textMuted,axis:c.border,grid:c.grid}),P>=0&&(t.fillStyle=c.bgHover,t.globalAlpha=.5,t.fillRect(m+P*A,F,A,J),t.globalAlpha=1);const d=M.map((l,u)=>({s:l,i:u})).sort((l,u)=>(l.s.groupKey===y?1:0)-(u.s.groupKey===y?1:0));for(const{s:l,i:u}of d){const Z=y===l.groupKey,K=y!==null&&!Z,L=D(u);t.strokeStyle=L,t.fillStyle=L,t.lineWidth=Z?2:1,t.globalAlpha=K?.12:y?.32:.22,t.beginPath(),t.moveTo(m,k),l.counts.forEach((tt,st)=>{const N=m+st*A,Y=k-tt/j*J;t.lineTo(N,Y),t.lineTo(N+A,Y)}),t.lineTo(m+v,k),t.closePath(),t.fill(),t.globalAlpha=K?.3:1,t.beginPath(),l.counts.forEach((tt,st)=>{const N=m+st*A,Y=k-tt/j*J;st===0?t.moveTo(N,Y):t.lineTo(N,Y),t.lineTo(N+A,Y)}),t.stroke()}t.globalAlpha=1;const g=l=>m+(l-X)/ht*v;t.font=`${R(-1)}px system-ui, sans-serif`,Gt(t,V,new Set,g,F,k,c.limitLine),t.font=`${R(-1)}px system-ui, sans-serif`,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.moveTo(m,k),t.lineTo(m+v,k),t.stroke(),t.fillStyle=c.textMuted,t.textAlign="center",t.textBaseline="top";const{ticks:x,step:e}=Et(X,ct,v,Pt(t,dt,T)),f=Tt(dt,T,e||void 0);for(const l of x){const u=m+(l-X)/ht*v;t.beginPath(),t.moveTo(u,k),t.lineTo(u,k+4),t.stroke(),t.fillText(f.tick(l),u,k+6)}f.unitLabel&&(t.save(),t.font=`${R(-1)}px system-ui, sans-serif`,t.fillStyle=c.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${f.unitLabel})`,m+v,k+20),t.restore())}function H(o){const{plotX:p,plotMaxWidth:r,barWidth:t}=ot;if(o<p||o>p+r)return-1;const c=Math.floor((o-p)/t);return c>=0&&c<w.length?c:-1}S.addEventListener("mousemove",o=>{const p=S.getBoundingClientRect(),r=H(o.clientX-p.left);if(r!==P&&(P=r,O()),r>=0){const t=w[r],c=M.map((m,v)=>`<span style="${he(D(v))};margin-right:4px"></span>${Mt(`${m.groupKey}: ${m.counts[r]}`)}`).join("<br>");B.innerHTML=`<strong>${Mt(`${gt(t.rangeLow,T,"engineering")} \u2013 ${gt(t.rangeHigh,T,"engineering")}`)}</strong><br>${c}`,B.style.display="block",Bt(B,s,o.clientX,o.clientY)}else B.style.display="none"}),S.addEventListener("mouseleave",()=>{P!==-1&&(P=-1,O()),B.style.display="none"}),rt=Wt(s,()=>O()),O()}q();function Yt(a){kt.set(a)&&(i=a,q())}function Qt(a){at.set(a)&&q()}return{card:s,setTest:Yt,setAxisPrefs:Qt,destroy:()=>rt?.disconnect()}}
@@ -1,7 +1,7 @@
1
1
  import { type ScatterItem, type ScatterPoint } from '../../stats/scatter.js';
2
2
  import { type FacetItem } from '../../stats/facets.js';
3
3
  import type { TestDef } from '../../renderer/buildWaferMap.js';
4
- import { type SaveImageHandler } from './chartShell.js';
4
+ import { type SaveImageHandler, type AxisPrefs } from './chartShell.js';
5
5
  type ScatterPanelItem = FacetItem & ScatterItem & {
6
6
  label?: string;
7
7
  };
@@ -34,6 +34,14 @@ export interface ScatterPanelOptions {
34
34
  key: string;
35
35
  items: ScatterPanelItem[];
36
36
  }[];
37
+ /**
38
+ * The Insights tab's shared axis preferences. Scatter reads only `limits`
39
+ * (which limit lines to draw), and offers the "Limits:" choice when either
40
+ * axis's test has both test and spec limits.
41
+ */
42
+ axisPrefs?: AxisPrefs;
43
+ /** Called when the user changes the "Limits:" choice here. */
44
+ onAxisPrefsChange?: (prefs: AxisPrefs) => void;
37
45
  /** Document to build this panel's DOM into. Default `document` — pass the
38
46
  * host's own `ownerDocument` when the container might live in a
39
47
  * different document (e.g. a gallery card detached into its own popup
@@ -44,6 +52,8 @@ export interface ScatterPanelHandle {
44
52
  card: HTMLElement;
45
53
  /** Cross-panel link (e.g. from the correlation matrix): switch X/Y in place. */
46
54
  setXY: (xTestNumber: number, yTestNumber: number) => void;
55
+ /** Adopt axis preferences chosen in another panel (only `limits` applies). */
56
+ setAxisPrefs: (prefs: AxisPrefs) => void;
47
57
  destroy: () => void;
48
58
  }
49
59
  export declare function renderScatterPanel(options: ScatterPanelOptions): ScatterPanelHandle;