@wafertools/wafermap 0.30.3 → 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 (85) hide show
  1. package/AGENTS.md +9 -19
  2. package/CHANGELOG.md +140 -0
  3. package/README.md +2 -2
  4. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +89 -6
  7. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  9. package/dist/packages/canvas-adapter/charts/scatter.d.ts +16 -2
  10. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/sweep.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  13. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  14. package/dist/packages/canvas-adapter/dieList.d.ts +1 -1
  15. package/dist/packages/canvas-adapter/exportName.d.ts +1 -12
  16. package/dist/packages/canvas-adapter/exportName.js +1 -1
  17. package/dist/packages/canvas-adapter/index.d.ts +2 -3
  18. package/dist/packages/canvas-adapter/index.js +1 -1
  19. package/dist/packages/canvas-adapter/insightsTab.d.ts +11 -1
  20. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  21. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  22. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +4 -7
  23. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  24. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +15 -30
  25. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  26. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  27. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  28. package/dist/packages/canvas-adapter/toolbar.js +1 -1
  29. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  30. package/dist/packages/canvas-adapter/userGuideHtml.js +49 -43
  31. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  32. package/dist/packages/canvas-adapter/version.js +1 -1
  33. package/dist/packages/canvas-adapter/warnings.js +1 -1
  34. package/dist/packages/core/aggregates.d.ts +0 -6
  35. package/dist/packages/core/aggregates.js +1 -1
  36. package/dist/packages/core/dies.d.ts +1 -3
  37. package/dist/packages/core/dies.js +1 -1
  38. package/dist/packages/core/index.d.ts +8 -10
  39. package/dist/packages/core/index.js +1 -1
  40. package/dist/packages/core/stdf.d.ts +9 -0
  41. package/dist/packages/core/stdf.js +1 -0
  42. package/dist/packages/core/transforms.d.ts +0 -23
  43. package/dist/packages/core/transforms.js +1 -1
  44. package/dist/packages/renderer/binColors.js +1 -1
  45. package/dist/packages/renderer/buildView.d.ts +1 -9
  46. package/dist/packages/renderer/buildView.js +1 -1
  47. package/dist/packages/renderer/buildWaferMap.d.ts +54 -34
  48. package/dist/packages/renderer/buildWaferMap.js +1 -1
  49. package/dist/packages/renderer/colorMap.d.ts +0 -2
  50. package/dist/packages/renderer/colorMap.js +1 -1
  51. package/dist/packages/renderer/deprecate.d.ts +1 -1
  52. package/dist/packages/renderer/deprecate.js +1 -1
  53. package/dist/packages/renderer/derivedTests/apply.js +1 -1
  54. package/dist/packages/renderer/derivedTests/parser.js +1 -1
  55. package/dist/packages/renderer/index.d.ts +6 -5
  56. package/dist/packages/renderer/index.js +1 -1
  57. package/dist/packages/renderer/spec.d.ts +17 -0
  58. package/dist/packages/renderer/spec.js +1 -1
  59. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  60. package/dist/packages/stats/capability.d.ts +4 -2
  61. package/dist/packages/stats/capability.js +1 -1
  62. package/dist/packages/stats/clusterDetection.d.ts +2 -1
  63. package/dist/packages/stats/correlation.d.ts +1 -1
  64. package/dist/packages/stats/facets.d.ts +1 -1
  65. package/dist/packages/stats/index.d.ts +6 -16
  66. package/dist/packages/stats/index.js +1 -1
  67. package/dist/packages/stats/mergeTestDefs.d.ts +1 -1
  68. package/dist/packages/stats/mergeTestDefs.js +1 -1
  69. package/dist/packages/stats/patternClassification.d.ts +2 -1
  70. package/dist/packages/stats/regions.d.ts +2 -1
  71. package/dist/packages/stats/renderFindingsReport.d.ts +0 -4
  72. package/dist/packages/stats/renderFindingsReport.js +1 -21
  73. package/dist/packages/stats/renderSummaryReport.js +11 -11
  74. package/dist/packages/stats/sweep.d.ts +8 -0
  75. package/dist/packages/stats/sweep.js +1 -1
  76. package/dist/packages/stats/types.d.ts +9 -3
  77. package/package.json +2 -2
  78. package/dist/packages/canvas-adapter/deprecated.d.ts +0 -7
  79. package/dist/packages/canvas-adapter/deprecated.js +0 -1
  80. package/dist/packages/core/deprecated.d.ts +0 -60
  81. package/dist/packages/core/deprecated.js +0 -1
  82. package/dist/packages/renderer/deprecated.d.ts +0 -45
  83. package/dist/packages/renderer/deprecated.js +0 -1
  84. package/dist/packages/stats/deprecated.d.ts +0 -85
  85. 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,6 +22,146 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
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.
118
+
119
+ ## [0.30.4] — 2026-09-25
120
+
121
+ ### Added
122
+
123
+ - **`input-values-outside-stdf` warning** from `buildWaferMap`: bins, coordinates, test
124
+ numbers or site numbers outside the STDF V4 ranges, test values that are not finite, and a
125
+ `waferConfig.orientation` other than 0, 90, 180 or 270. The values are used as given; a
126
+ future release treats them as missing.
127
+ - **`DieResult.supersedes`** (`'partId' | 'position'`): the tester marked the record as
128
+ replacing an earlier one. Such a record always wins, whatever `retestPolicy` says.
129
+ - **Retests of dies with no position are matched by part ID** within a wafer, resolved by
130
+ `retestPolicy` and reported with an `info` warning, `retests-by-part-id`. Blank part IDs
131
+ never match, and part IDs are not used on a wafer where one value covers more than 20% of
132
+ the unpositioned records.
133
+ - **`TestDef.specLow` / `TestDef.specHigh`**: a test's spec limits, separate from its test
134
+ limits (`limitLow`/`limitHigh`), as STDF records them. Process capability uses the spec
135
+ limits when both are given and the test limits otherwise, and reports which as
136
+ `TestCapability.limitBasis`. Wafers that disagree on a test's spec limits raise a
137
+ `specLimits` test-definition conflict.
138
+ - **Sweeps tab notice for sweeps that name no test in the data**, and
139
+ **`InsightsOptions.onRemoveSweeps`**, which adds a Remove button to it. The sweep's own card
140
+ says the same in place of an empty chart.
141
+ - **`TestDef.limitLowInclusive` / `TestDef.limitHighInclusive`**: whether a value exactly
142
+ equal to a test limit passes (default `true`), as STDF and ATDF state per test. Every
143
+ pass/fail judgement, yield and out-of-limit marker honours them. Wafers that disagree on
144
+ them raise a `limits` test-definition conflict.
145
+
146
+ ### Changed
147
+
148
+ - **`DieResult.partId` and `Die.partId` accept text** (`number | string`), as STDF defines
149
+ part IDs; a number is still accepted.
150
+ - **Scatter chart legend:** hard bin 0 has its own entry and colour, and dies without a bin
151
+ are listed last as "No bin data".
152
+ - **A `NaN` bin is no bin**: it is neither a bin number nor a fail, and is counted in the new
153
+ warning.
154
+ - **One spec-limit judgement** (`classifySpec`) serves every surface, including the per-test
155
+ spec yield, lot-stack fail filtering, regional spec-fail findings and the Summary panel.
156
+ A value that is not a finite number has no verdict everywhere, rather than counting as a
157
+ pass on some surfaces.
158
+ - **Bin colours** for a bin that is not a whole number are taken from the whole bin below it.
159
+ - **Test limits and spec limits are labelled apart.** `limitLow`/`limitHigh` are test limits,
160
+ labelled "Lo limit"/"Hi limit" on charts; LSL/USL appear only where a test's spec limits
161
+ are shown. The pass/fail option is "Limit pass/fail", the Summary panel's yield column is
162
+ "Limit yield", and the capability chart, tooltip and report name the limits each test was
163
+ judged against.
164
+
25
165
  ## [0.30.3] — 2026-09-23
26
166
 
27
167
  ### Added
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-~59%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 ~59 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 L,VALUE_WIDTH as vt,WAFER_MENU_HINT as kt,prepareCanvas as Lt}from"./chartShell.js";import{escHtml as M,maxOf as Mt,minOf as Ct}from"../../core/utils.js";const C=24,S=5,We=110,St=12,Oe=20;export function renderBoxplotPanel(u){const{title:ee="Test value distribution",items:Ee,testDefs:ge,onSaveImage:Pe,groups:g,groupLabelText:te="group",onOpen:U}=u,_e=u.openActionLabel??"open that wafer",Ge=u.openActionLabel??"open this wafer",{card:a,heading:Ne,body:x,controlsRow:z}=st(ee,Pe,u.ownerDocument);pt(a,"rows"),a.style.flex="0 0 auto",x.style.flex="0 0 auto",a.style.alignSelf="start";const ne=ge.filter(i=>i.testNumber!==void 0);let y=u.selectedTestNumber??ne[0]?.testNumber??null,ie=!1,Q=0,b=null,R=null,oe=null,be=null;function Xe(){const i=!g||g.length===0?"":b??"\0overview";if(oe&&be===i)return oe;const p=!g||g.length===0?Ee:b!==null?g.find(h=>h.key===b)?.items??[]:g.map(h=>({label:h.key,dies:h.items.flatMap(f=>f.dies??[])}));return oe=p,be=i,p}function Te(){b!==null&&!R?(R=ft(()=>{ye(null),u.onGroupChange?.(null)},a.ownerDocument),z.appendChild(R)):b===null&&R&&(R.remove(),R=null),Ne.textContent=b!==null?`${ee} \u2014 ${te}: ${b}`:ee}function ye(i){return b===i||i!==null&&!(g??[]).some(p=>p.key===i)?!1:(b=i,Te(),F(),A(),!0)}const we=ut(ne,y,i=>{y=i,A(),u.onTestChange?.(i)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),qe=we.el;z.appendChild(qe),z.appendChild(mt("Log scale",ie,i=>{ie=i,A()},a.ownerDocument));const le=a.ownerDocument.createElement("span");Object.assign(le.style,{display:"inline-flex",gap:Be.lg,alignItems:"center"}),z.appendChild(le);const se=dt(le,u.axisPrefs,i=>{u.onAxisPrefsChange?.(i),A()},a.ownerDocument),ae=a.ownerDocument.createElement("div");Object.assign(ae.style,{color:ot.label,fontSize:it.body,marginBottom:Be.sm}),a.insertBefore(ae,x);function F(){const i=!!g&&g.length>0&&b===null,p=[];i?p.push(`click a ${te}'s box to see it by wafer`):U&&p.push(`click a box to ${_e}`);const h=p.length?`${p[0][0].toUpperCase()}${p[0].slice(1)} \xB7 `:"";ae.textContent=`${h}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(Q?` \xB7 axis clipped, ${Q} value${Q===1?"":"s"} outside`:"")}F(),Te();const D=rt(a);let re=null;function A(){const{includeLimits:i,clipOutliers:p}=se.get();if(F(),x.innerHTML="",ne.length===0||y===null){Re(x,"No parametric test data available for box plots.");return}const h=Xe(),f=nt(h,y),ce=ge.find(t=>t.testNumber===y),W=ce?.unit,V=ce?.limitLow,j=ce?.limitHigh;if(f.every(t=>t.count===0)){Re(x,"No parametric test data available for box plots.");return}const m=a.ownerDocument.createElement("canvas");m.style.display="block",m.style.cursor="default",x.appendChild(m);const ve=L*2+Math.min(f.length,St)*(C+S)+Oe;x.style.maxHeight=`${ve}px`,x.style.overflowY="auto",x.style.scrollbarGutter="stable";let P=-1;const fe=f.filter(t=>t.count>0),ke=Ct(fe.map(t=>t.min)),Le=Mt(fe.map(t=>t.max)),Me=i??Tt(ke,Le,V,j);se.sync(Me,V!==void 0||j!==void 0);const _=bt({dataMin:ke,dataMax:Le,limitLow:V,limitHigh:j,includeLimits:Me,clipOutliers:p,values:fe.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});Q=_.clippedCount,F();const O=_.lo,K=_.hi,ze=K-O||1,J=ie&&O>0,ue=J?Math.log10(O):0,Ce=(J?Math.log10(K):0)-ue||1,me=Math.max(Math.abs(O),Math.abs(K));function Qe(){const t=L+We,s=m.clientWidth-t-vt-L;return{plotX:t,plotMaxWidth:Math.max(10,s)}}function $(t,s,n){return J?s+(Math.log10(t)-ue)/Ce*n:s+(t-O)/ze*n}function Fe(t,s){if(J){const d=[];for(let e=0;e<=4;e++)d.push(Math.pow(10,ue+Ce*e/4));return{ticks:d,axis:De(me,W)}}const n=lt(O,K,s,gt(t,me,W));return{ticks:n.ticks,axis:De(me,W,n.step||void 0)}}function Z(){const t=ht(a),s=L*2+f.length*(C+S)+Oe;x.style.maxHeight=`${xt(a,x,ve,s)}px`;const n=x.clientWidth,d=Lt(m,a,n,s);if(!d)return;const{ctx:e}=d;e.font=`${Y(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:r}=Qe();f.forEach((o,T)=>{const v=L+T*(C+S),c=v+C/2;T===P&&(e.fillStyle=t.bgHover,e.fillRect(0,v-S/2,n,C+S)),e.fillStyle=t.text,e.textAlign="right";const Ke=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(Ke,L+We-8,c),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,c);return}const Je=l+r,N=tt=>Math.min(Je,Math.max(l,tt)),Ae=$(o.min,l,r),$e=$(o.max,l,r),I=N(Ae),X=N($(o.q1,l,r)),Ie=N($(o.median,l,r)),pe=N($(o.q3,l,r)),B=N($e),Ze=Ae<I,et=$e>B,H=v+3,q=v+C-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(I,c),e.lineTo(X,c),e.moveTo(pe,c),e.lineTo(B,c);const k=3;Ze?(e.moveTo(I+k,c-k),e.lineTo(I,c),e.lineTo(I+k,c+k)):(e.moveTo(I,H),e.lineTo(I,q)),et?(e.moveTo(B-k,c-k),e.lineTo(B,c),e.lineTo(B-k,c+k)):(e.moveTo(B,H),e.lineTo(B,q)),e.stroke(),e.fillStyle=He,e.globalAlpha=.35,e.fillRect(X,H,Math.max(1,pe-X),q-H),e.globalAlpha=1,e.strokeStyle=He,e.strokeRect(X,H,Math.max(1,pe-X),q-H),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Ie,H),e.lineTo(Ie,q),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(he(o.median,W,"engineering"),l+r+10,c)});const w=L+f.length*(C+S);e.font=`${Y(-1)}px system-ui, sans-serif`,yt(e,_.offAxis,{left:l,right:l+r,top:0,bottom:w},"horizontal",t.limitLine,o=>he(o,W,"engineering"));const Ve=new Set(_.offAxis.map(o=>o.value));for(const[o,T]of[[V,"LSL"],[j,"USL"]]){if(o===void 0||Ve.has(o))continue;const v=$(o,l,r);e.strokeStyle=t.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(v,0),e.lineTo(v,w),e.stroke(),e.setLineDash([]),e.fillStyle=t.limitLine;const c=wt(v,e.measureText(T).width,l,l+r,T==="LSL");e.textAlign=c<0?"right":"left",e.textBaseline="bottom",e.fillText(T,v+c*3,w-1)}e.font=`${Y(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,w),e.lineTo(l+r,w),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:je,axis:xe}=Fe(e,r);for(const o of je){const T=$(o,l,r);e.beginPath(),e.moveTo(T,w),e.lineTo(T,w+4),e.stroke(),e.fillText(xe.tick(o),T,w+6)}xe.unitLabel&&(e.textAlign="left",e.font=`${Y(-1)}px system-ui, sans-serif`,e.fillText(`(${xe.unitLabel})`,l+r+22,w+6),e.font=`${Y(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function de(t){const s=Math.floor((t-L+S/2)/(C+S));return s>=0&&s<f.length?s:-1}function G(t){return he(t,W,"engineering")}const E=!!g&&g.length>0&&b===null,Se=t=>!E&&!!U&&h[t]?.waferIndex!==void 0;m.addEventListener("mousemove",t=>{const s=m.getBoundingClientRect(),n=de(t.clientY-s.top),d=n>=0&&f[n].count>0&&(E||Se(n));if(n!==P&&(P=n,m.style.cursor=d?"pointer":"default",Z()),n>=0&&f[n].count>0){const e=f[n],l=E?`<br><em>click to see this ${M(te)} by wafer</em>`:Se(n)?`<br><em>click to ${M(Ge)}</em>`:"",r=!E&&u.onWaferContextMenu&&h[n]?.waferIndex!==void 0?kt:"";D.innerHTML=`<strong>${M(e.label)}</strong> (${e.count} dies)<br>max ${M(G(e.max))}<br>q3 ${M(G(e.q3))}<br>median ${M(G(e.median))}<br>q1 ${M(G(e.q1))}<br>min ${M(G(e.min))}${l}${r}`,D.style.display="block",ct(D,a,t.clientX,t.clientY)}else D.style.display="none"}),m.addEventListener("mouseleave",()=>{P!==-1&&(P=-1,Z()),D.style.display="none"}),m.addEventListener("click",t=>{const s=m.getBoundingClientRect(),n=de(t.clientY-s.top);if(n===-1||f[n].count===0)return;if(E){const e=f[n].label;ye(e),u.onGroupChange?.(e);return}const d=h[n]?.waferIndex;U&&d!==void 0&&y!==null&&U(d,y)}),m.addEventListener("contextmenu",t=>{const s=m.getBoundingClientRect(),n=de(t.clientY-s.top),d=n===-1?void 0:h[n]?.waferIndex;E||d===void 0||!u.onWaferContextMenu||(D.style.display="none",u.onWaferContextMenu(d,y??void 0,t))}),re?.disconnect(),re=at(a,()=>Z()),Z()}A();function Ye(i){we.set(i)&&(y=i,A())}function Ue(i){se.set(i)&&A()}return{card:a,setTest:Ye,setAxisPrefs:Ue,destroy:()=>re?.disconnect()}}
1
+ 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()}}
@@ -1 +1 @@
1
- import{buildCapabilityData as xe}from"../../stats/capability.js";import{capabilityColor as j}from"./palette.js";import{LEADING as ge,SPACE as S,fontPx as K,FONT as Q,CLR as R}from"../toolbar.js";import{cardShell as ve,isExpandedCard as Se,chartFillHeight as Me,applyCanvasFlow as Te,observeResize as Ce,makeTooltip as Le,positionChartTooltip as ke,renderEmptyState as $e,resolveChartCanvasColors as Ee,chartSwatchCss as Pe,prepareCanvas as De,chartDpr as Ae}from"./chartShell.js";import{fmt as We}from"../../renderer/fmt.js";import{escHtml as z}from"../../core/utils.js";import{DERIVED_MARK as Y,DERIVED_KEY as oe}from"../../renderer/testLabel.js";const we=30,le=160,se=90,ie=28,T=42;function O(E){return E===null?"\u2014":E.toFixed(2)}export function renderCapabilityPanel(E){const{title:ae="Process capability",items:re,testDefs:X,onSaveImage:ce,onSelectTest:_}=E;let F=E.selectedTestNumber??null;const{card:r,body:C}=ve(ae,ce,E.ownerDocument),me=X.some(n=>n.testNumber!==void 0);C.style.overflowX="auto";function de(){return re}const P=r.ownerDocument.createElement("div");Object.assign(P.style,{display:"flex",flexDirection:"column",gap:S.xs,marginBottom:S.sm}),r.insertBefore(P,C);const A=Le(r);let B=()=>{};function pe(n,i,x,h){P.innerHTML="";const b=r.ownerDocument.createElement("span");Object.assign(b.style,{display:"inline-flex",alignItems:"center",gap:S.sm,color:R.value,fontSize:Q.body,fontWeight:"500"});const L=x-n,m=i>0?` \xB7 ${i} without spec limits`:"",u=r.ownerDocument.createElement("span");u.textContent=L>0?`${n} of ${x} tests shown${m} \xB7 ${L} excluded (no recorded values)`:`${n} test${n!==1?"s":""} shown${m}`,b.appendChild(u),P.appendChild(b);const g=i>=n,M=r.ownerDocument.createElement("div");M.textContent=g?"No test has both spec limits, so each is normalised to its own observed range (min = 0, max = 1) and sorted most-variable first. Ppk needs limits, so none is shown.":"Normalised to spec limits (LSL = 0, USL = 1), worst Ppk first. Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(M.style,{color:R.label,fontSize:Q.body,lineHeight:ge.base,marginTop:S.xs,maxWidth:"78ch"}),P.appendChild(M),P.appendChild(fe(r.ownerDocument,i>0,g,h>0))}function fe(n,i,x=!1,h=!1){const b=n.createElement("div");Object.assign(b.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${S.xs} ${S.xl}`,marginTop:S.xl,marginBottom:S.xs,fontSize:Q.body,color:R.label});const L=x?[]:[{color:j(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:j(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:j(.5),label:"Poor \xB7 Ppk < 1.0"}];i&&L.push({color:R.label,label:"No spec limits \xB7 no Ppk",dashed:!0});for(const m of L){const u=n.createElement("span");Object.assign(u.style,{display:"inline-flex",alignItems:"center",gap:S.sm});const g=n.createElement("span");g.style.cssText=Pe(m.color,m.dashed?"outline":"solid");const M=n.createElement("span");M.textContent=m.label,u.append(g,M),b.appendChild(u)}if(h){const m=n.createElement("span");Object.assign(m.style,{display:"inline-flex",alignItems:"center",gap:S.sm});const u=n.createElement("span");u.textContent=Y,Object.assign(u.style,{color:R.value,fontWeight:"600"});const g=n.createElement("span");g.textContent=oe,m.append(u,g),b.appendChild(m)}return b}function he(n){if(C.innerHTML="",n.length===0)return $e(C,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const i=r.ownerDocument.createElement("canvas");i.style.display="block",i.style.cursor="default",C.appendChild(i);const x=n.length,h=Math.min(0,...n.map(t=>t.min)),b=Math.max(1,...n.map(t=>t.max)),L=(b-h)*.05||.1,m=h-L,g=b+L-m||1;function M(t){const l=Math.floor(t/x);return Math.max(we,Se(r)?l:Math.min(le,l))}function k(t,l,o){return l+(1-(t-m)/g)*o}function ye(t,l,o,s,$){const y=n.filter(a=>a.hasSpec).length,D=y===n.length,d=y===0,H=d?"max":"USL",e=d?"min":"LSL",p=D?"normalised to spec":d?"normalised to range":"normalised (per test)";t.font=`${K(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle";for(const[a,w]of[[1,H],[0,e]]){const f=k(a,o,s);f<o-1||f>o+s+1||(t.strokeStyle=l.limitLine,t.lineWidth=1,t.beginPath(),t.moveTo(T-5,f),t.lineTo(T,f),t.stroke(),t.fillStyle=l.textMuted,t.fillText(w,T-8,f))}t.save(),t.translate(9,o+s/2),t.rotate(-Math.PI/2),t.textAlign="center",t.fillStyle=l.textMuted,t.fillText(p,0,0),t.restore()}let W=-1;function U(){Te(i);const t=C.clientWidth,l=M(t-T),o=l*x,s=ie,$=T+o,y=Me(r,C,i,ie+200+se),D=Math.max(s+60,y-se),d=D-s,H=De(i,r,$,y);if(!H)return;const{ctx:e}=H,p=Ee(r);n.forEach((a,w)=>{const f=T+w*l,c=f+l/2,v=Math.max(4,Math.min(l,le)*.55);if(w===W&&(e.fillStyle=p.bgHover,e.fillRect(f,s,l,D-s)),n[w].testNumber===F&&(e.fillStyle=p.accent,e.fillRect(f,D-2,l,2)),a.hasSpec){const te=k(0,s,d),ne=k(1,s,d);e.strokeStyle=p.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(f,te),e.lineTo(f+l,te),e.moveTo(f,ne),e.lineTo(f+l,ne),e.stroke(),e.setLineDash([])}const V=a.hasSpec?j(a.ppk):p.textMuted,q=k(a.min,s,d),N=k(a.q1,s,d),Z=k(a.median,s,d),I=k(a.q3,s,d),G=k(a.max,s,d);e.strokeStyle=p.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(c,q),e.lineTo(c,N),e.moveTo(c,I),e.lineTo(c,G),e.moveTo(c-v/4,q),e.lineTo(c+v/4,q),e.moveTo(c-v/4,G),e.lineTo(c+v/4,G),e.stroke(),e.fillStyle=V,e.globalAlpha=.5,e.fillRect(c-v/2,Math.min(N,I),v,Math.max(1,Math.abs(I-N))),e.globalAlpha=1,e.strokeStyle=V,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(c-v/2,Math.min(N,I),v,Math.max(1,Math.abs(I-N))),e.setLineDash([]),e.strokeStyle=p.text,e.lineWidth=2,e.beginPath(),e.moveTo(c-v/2,Z),e.lineTo(c+v/2,Z),e.stroke(),e.lineWidth=1,e.font=`${K(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?V:p.textMuted,e.fillText(a.hasSpec?O(a.ppk):"\u2014",c,s-8);const ee=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(c,D+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${K(-1)}px system-ui, sans-serif`,e.fillStyle=p.textMuted,e.fillText(ee,0,0),a.derived&&(e.fillStyle=p.text,e.fillText(`${Y} `,-e.measureText(ee).width,0)),e.restore()}),e.strokeStyle=p.border,e.lineWidth=1,e.strokeRect(T,s,o,d),ye(e,p,s,d,o)}function J(t){const l=i.getBoundingClientRect(),o=C.clientWidth,s=M(o-T),$=(t.clientX-l.left)*(i.width/Ae(i)/l.width)-T,y=Math.floor($/s);return y>=0&&y<x?y:-1}return i.addEventListener("mousemove",t=>{const l=J(t);if(l!==W&&(W=l,U()),l===-1){A.style.display="none";return}const o=n[l],s=y=>z(We(y,o.unit)),$=o.derived?`<em>${Y} ${z(oe)}</em>`+(o.expression?`<br><code>${z(o.expression)}</code>`:"")+"<br>":"";A.innerHTML=`<strong>${z(o.label)}</strong> (n=${o.n})<br>${$}`+(o.hasSpec?`LSL ${s(o.lsl)} \xB7 USL ${s(o.usl)}<br>mean ${s(o.mean)}<br>Cp ${O(o.cp)} \xB7 Cpk ${O(o.cpk)}<br>Pp ${O(o.pp)} \xB7 Ppk ${O(o.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${s(o.mean)} \xB7 stddev ${s(o.stdOverall)}`)+(_?"<br><em>click to view in boxplot</em>":""),A.style.display="block",ke(A,r,t.clientX,t.clientY),i.style.cursor=_?"pointer":"default"}),i.addEventListener("mouseleave",()=>{W!==-1&&(W=-1,U()),A.style.display="none"}),i.addEventListener("click",t=>{if(!_)return;const l=J(t);l!==-1&&_(n[l].testNumber)}),U}function be(){const n=xe(de(),X),i=X.filter(h=>h.testNumber!==void 0).length,x=n.filter(h=>!h.hasSpec).length;pe(n.length,x,i,n.filter(h=>h.derived).length),B=he(n),B()}const ue=Ce(r,()=>B());return be(),{card:r,hasData:me,setTest:n=>{F!==n&&(F=n,B())},destroy:()=>ue.disconnect()}}
1
+ import{buildCapabilityData as ve}from"../../stats/capability.js";import{capabilityColor as U}from"./palette.js";import{LEADING as Se,SPACE as T,fontPx as J,FONT as Z,CLR as _}from"../toolbar.js";import{cardShell as Me,isExpandedCard as Te,chartFillHeight as $e,applyCanvasFlow as Ce,observeResize as Le,makeTooltip as ke,positionChartTooltip as Pe,renderEmptyState as Ee,resolveChartCanvasColors as De,chartSwatchCss as Ae,prepareCanvas as We,chartDpr as we}from"./chartShell.js";import{fmt as Ne}from"../../renderer/fmt.js";import{escHtml as V}from"../../core/utils.js";import{DERIVED_MARK as ee,DERIVED_KEY as le}from"../../renderer/testLabel.js";function N(v){return v==="spec"?{lo:"LSL",hi:"USL",noun:"spec limits"}:{lo:"Lo limit",hi:"Hi limit",noun:"test limits"}}function ae(v){let D;for(const z of v){if(!z.hasSpec)continue;const A=z.limitBasis??"test";if(D===void 0)D=A;else if(D!==A)return"mixed"}return D}const Ie=30,re=160,ce=90,me=28,C=42;function j(v){return v===null?"\u2014":v.toFixed(2)}export function renderCapabilityPanel(v){const{title:D="Process capability",items:z,testDefs:A,onSaveImage:de,onSelectTest:X}=v;let q=v.selectedTestNumber??null;const{card:m,body:L}=Me(D,de,v.ownerDocument),pe=A.some(t=>t.testNumber!==void 0);L.style.overflowX="auto";function he(){return z}const W=m.ownerDocument.createElement("div");Object.assign(W.style,{display:"flex",flexDirection:"column",gap:T.xs,marginBottom:T.sm}),m.insertBefore(W,L);const I=ke(m);let F=()=>{};function fe(t,l,S,f,u){W.innerHTML="";const $=m.ownerDocument.createElement("span");Object.assign($.style,{display:"inline-flex",alignItems:"center",gap:T.sm,color:_.value,fontSize:Z.body,fontWeight:"500"});const d=S-t,y=l>0?` \xB7 ${l} without both limits`:"",M=m.ownerDocument.createElement("span");M.textContent=d>0?`${t} of ${S} tests shown${y} \xB7 ${d} excluded (no recorded values)`:`${t} test${t!==1?"s":""} shown${y}`,$.appendChild(M),W.appendChild($);const k=l>=t,b=m.ownerDocument.createElement("div");b.textContent=k?"No test has both 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.":(u==="mixed"?"Normalised to each test's spec limits (LSL = 0, USL = 1), or its test limits (Lo limit = 0, Hi limit = 1) where it has no spec limits, worst Ppk first. ":`Normalised to ${N(u).noun} (${N(u).lo} = 0, ${N(u).hi} = 1), worst Ppk first. `)+"Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(b.style,{color:_.label,fontSize:Z.body,lineHeight:Se.base,marginTop:T.xs,maxWidth:"78ch"}),W.appendChild(b),W.appendChild(ue(m.ownerDocument,l>0,k,f>0))}function ue(t,l,S=!1,f=!1){const u=t.createElement("div");Object.assign(u.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${T.xs} ${T.xl}`,marginTop:T.xl,marginBottom:T.xs,fontSize:Z.body,color:_.label});const $=S?[]:[{color:U(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:U(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:U(.5),label:"Poor \xB7 Ppk < 1.0"}];l&&$.push({color:_.label,label:"No limits \xB7 no Ppk",dashed:!0});for(const d of $){const y=t.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:T.sm});const M=t.createElement("span");M.style.cssText=Ae(d.color,d.dashed?"outline":"solid");const k=t.createElement("span");k.textContent=d.label,y.append(M,k),u.appendChild(y)}if(f){const d=t.createElement("span");Object.assign(d.style,{display:"inline-flex",alignItems:"center",gap:T.sm});const y=t.createElement("span");y.textContent=ee,Object.assign(y.style,{color:_.value,fontWeight:"600"});const M=t.createElement("span");M.textContent=le,d.append(y,M),u.appendChild(d)}return u}function be(t){if(L.innerHTML="",t.length===0)return Ee(L,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const l=m.ownerDocument.createElement("canvas");l.style.display="block",l.style.cursor="default",L.appendChild(l);const S=t.length,f=Math.min(0,...t.map(n=>n.min)),u=Math.max(1,...t.map(n=>n.max)),$=(u-f)*.05||.1,d=f-$,M=u+$-d||1;function k(n){const o=Math.floor(n/S);return Math.max(Ie,Te(m)?o:Math.min(re,o))}function b(n,o,i){return o+(1-(n-d)/M)*i}function ge(n,o,i,s,P){const x=t.filter(g=>g.hasSpec).length,w=x===t.length,p=x===0,E=ae(t),e=E==="mixed"?{lo:"Lo",hi:"Hi",noun:"limits"}:N(E),h=p?"max":E==="spec"?e.hi:"Hi",a=p?"min":E==="spec"?e.lo:"Lo",H=w?`normalised to ${e.noun}`:p?"normalised to range":"normalised (per test)";n.font=`${J(-1)}px system-ui, sans-serif`,n.textAlign="right",n.textBaseline="middle";for(const[g,r]of[[1,h],[0,a]]){const c=b(g,i,s);c<i-1||c>i+s+1||(n.strokeStyle=o.limitLine,n.lineWidth=1,n.beginPath(),n.moveTo(C-5,c),n.lineTo(C,c),n.stroke(),n.fillStyle=o.textMuted,n.fillText(r,C-8,c))}n.save(),n.translate(9,i+s/2),n.rotate(-Math.PI/2),n.textAlign="center",n.fillStyle=o.textMuted,n.fillText(H,0,0),n.restore()}let B=-1;function G(){Ce(l);const n=L.clientWidth,o=k(n-C),i=o*S,s=me,P=C+i,x=$e(m,L,l,me+200+ce),w=Math.max(s+60,x-ce),p=w-s,E=We(l,m,P,x);if(!E)return;const{ctx:e}=E,h=De(m);t.forEach((a,H)=>{const g=C+H*o,r=g+o/2,c=Math.max(4,Math.min(o,re)*.55);if(H===B&&(e.fillStyle=h.bgHover,e.fillRect(g,s,o,w-s)),t[H].testNumber===q&&(e.fillStyle=h.accent,e.fillRect(g,w-2,o,2)),a.hasSpec){const oe=b(0,s,p),se=b(1,s,p);e.strokeStyle=h.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(g,oe),e.lineTo(g+o,oe),e.moveTo(g,se),e.lineTo(g+o,se),e.stroke(),e.setLineDash([])}const K=a.hasSpec?U(a.ppk):h.textMuted,Q=b(a.min,s,p),R=b(a.q1,s,p),ne=b(a.median,s,p),O=b(a.q3,s,p),Y=b(a.max,s,p);e.strokeStyle=h.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(r,Q),e.lineTo(r,R),e.moveTo(r,O),e.lineTo(r,Y),e.moveTo(r-c/4,Q),e.lineTo(r+c/4,Q),e.moveTo(r-c/4,Y),e.lineTo(r+c/4,Y),e.stroke(),e.fillStyle=K,e.globalAlpha=.5,e.fillRect(r-c/2,Math.min(R,O),c,Math.max(1,Math.abs(O-R))),e.globalAlpha=1,e.strokeStyle=K,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(r-c/2,Math.min(R,O),c,Math.max(1,Math.abs(O-R))),e.setLineDash([]),e.strokeStyle=h.text,e.lineWidth=2,e.beginPath(),e.moveTo(r-c/2,ne),e.lineTo(r+c/2,ne),e.stroke(),e.lineWidth=1,e.font=`${J(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?K:h.textMuted,e.fillText(a.hasSpec?j(a.ppk):"\u2014",r,s-8);const ie=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(r,w+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${J(-1)}px system-ui, sans-serif`,e.fillStyle=h.textMuted,e.fillText(ie,0,0),a.derived&&(e.fillStyle=h.text,e.fillText(`${ee} `,-e.measureText(ie).width,0)),e.restore()}),e.strokeStyle=h.border,e.lineWidth=1,e.strokeRect(C,s,i,p),ge(e,h,s,p,i)}function te(n){const o=l.getBoundingClientRect(),i=L.clientWidth,s=k(i-C),P=(n.clientX-o.left)*(l.width/we(l)/o.width)-C,x=Math.floor(P/s);return x>=0&&x<S?x:-1}return l.addEventListener("mousemove",n=>{const o=te(n);if(o!==B&&(B=o,G()),o===-1){I.style.display="none";return}const i=t[o],s=x=>V(Ne(x,i.unit)),P=i.derived?`<em>${ee} ${V(le)}</em>`+(i.expression?`<br><code>${V(i.expression)}</code>`:"")+"<br>":"";I.innerHTML=`<strong>${V(i.label)}</strong> (n=${i.n})<br>${P}`+(i.hasSpec?`${N(i.limitBasis).lo} ${s(i.lsl)} \xB7 ${N(i.limitBasis).hi} ${s(i.usl)}<br>mean ${s(i.mean)}<br>Cp ${j(i.cp)} \xB7 Cpk ${j(i.cpk)}<br>Pp ${j(i.pp)} \xB7 Ppk ${j(i.ppk)}`:`<em>No limits \u2014 sorted by variability</em><br>mean ${s(i.mean)} \xB7 stddev ${s(i.stdOverall)}`)+(X?"<br><em>click to view in boxplot</em>":""),I.style.display="block",Pe(I,m,n.clientX,n.clientY),l.style.cursor=X?"pointer":"default"}),l.addEventListener("mouseleave",()=>{B!==-1&&(B=-1,G()),I.style.display="none"}),l.addEventListener("click",n=>{if(!X)return;const o=te(n);o!==-1&&X(t[o].testNumber)}),G}function ye(){const t=ve(he(),A),l=A.filter(f=>f.testNumber!==void 0).length,S=t.filter(f=>!f.hasSpec).length;fe(t.length,S,l,t.filter(f=>f.derived).length,ae(t)),F=be(t),F()}const xe=Le(m,()=>F());return ye(),{card:m,hasData:pe,setTest:t=>{q!==t&&(q=t,F())},destroy:()=>xe.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: 'LSL' | 'USL';
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