@wafertools/wafermap 0.26.0 → 0.27.0

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