@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.
- package/AGENTS.md +9 -19
- package/CHANGELOG.md +140 -0
- package/README.md +2 -2
- package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
- package/dist/packages/canvas-adapter/charts/capability.js +1 -1
- package/dist/packages/canvas-adapter/charts/chartShell.d.ts +89 -6
- package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
- package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
- package/dist/packages/canvas-adapter/charts/scatter.d.ts +16 -2
- package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
- package/dist/packages/canvas-adapter/charts/sweep.js +1 -1
- package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
- package/dist/packages/canvas-adapter/charts/trend.js +1 -1
- package/dist/packages/canvas-adapter/dieList.d.ts +1 -1
- package/dist/packages/canvas-adapter/exportName.d.ts +1 -12
- package/dist/packages/canvas-adapter/exportName.js +1 -1
- package/dist/packages/canvas-adapter/index.d.ts +2 -3
- package/dist/packages/canvas-adapter/index.js +1 -1
- package/dist/packages/canvas-adapter/insightsTab.d.ts +11 -1
- package/dist/packages/canvas-adapter/insightsTab.js +1 -1
- package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +4 -7
- package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +15 -30
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
- package/dist/packages/canvas-adapter/toCanvas.js +1 -1
- package/dist/packages/canvas-adapter/toolbar.js +1 -1
- package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
- package/dist/packages/canvas-adapter/userGuideHtml.js +49 -43
- package/dist/packages/canvas-adapter/version.d.ts +2 -2
- package/dist/packages/canvas-adapter/version.js +1 -1
- package/dist/packages/canvas-adapter/warnings.js +1 -1
- package/dist/packages/core/aggregates.d.ts +0 -6
- package/dist/packages/core/aggregates.js +1 -1
- package/dist/packages/core/dies.d.ts +1 -3
- package/dist/packages/core/dies.js +1 -1
- package/dist/packages/core/index.d.ts +8 -10
- package/dist/packages/core/index.js +1 -1
- package/dist/packages/core/stdf.d.ts +9 -0
- package/dist/packages/core/stdf.js +1 -0
- package/dist/packages/core/transforms.d.ts +0 -23
- package/dist/packages/core/transforms.js +1 -1
- package/dist/packages/renderer/binColors.js +1 -1
- package/dist/packages/renderer/buildView.d.ts +1 -9
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.d.ts +54 -34
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/renderer/colorMap.d.ts +0 -2
- package/dist/packages/renderer/colorMap.js +1 -1
- package/dist/packages/renderer/deprecate.d.ts +1 -1
- package/dist/packages/renderer/deprecate.js +1 -1
- package/dist/packages/renderer/derivedTests/apply.js +1 -1
- package/dist/packages/renderer/derivedTests/parser.js +1 -1
- package/dist/packages/renderer/index.d.ts +6 -5
- package/dist/packages/renderer/index.js +1 -1
- package/dist/packages/renderer/spec.d.ts +17 -0
- package/dist/packages/renderer/spec.js +1 -1
- package/dist/packages/stats/analyzeWaferMap.js +1 -1
- package/dist/packages/stats/capability.d.ts +4 -2
- package/dist/packages/stats/capability.js +1 -1
- package/dist/packages/stats/clusterDetection.d.ts +2 -1
- package/dist/packages/stats/correlation.d.ts +1 -1
- package/dist/packages/stats/facets.d.ts +1 -1
- package/dist/packages/stats/index.d.ts +6 -16
- package/dist/packages/stats/index.js +1 -1
- package/dist/packages/stats/mergeTestDefs.d.ts +1 -1
- package/dist/packages/stats/mergeTestDefs.js +1 -1
- package/dist/packages/stats/patternClassification.d.ts +2 -1
- package/dist/packages/stats/regions.d.ts +2 -1
- package/dist/packages/stats/renderFindingsReport.d.ts +0 -4
- package/dist/packages/stats/renderFindingsReport.js +1 -21
- package/dist/packages/stats/renderSummaryReport.js +11 -11
- package/dist/packages/stats/sweep.d.ts +8 -0
- package/dist/packages/stats/sweep.js +1 -1
- package/dist/packages/stats/types.d.ts +9 -3
- package/package.json +2 -2
- package/dist/packages/canvas-adapter/deprecated.d.ts +0 -7
- package/dist/packages/canvas-adapter/deprecated.js +0 -1
- package/dist/packages/core/deprecated.d.ts +0 -60
- package/dist/packages/core/deprecated.js +0 -1
- package/dist/packages/renderer/deprecated.d.ts +0 -45
- package/dist/packages/renderer/deprecated.js +0 -1
- package/dist/packages/stats/deprecated.d.ts +0 -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()`,
|
|
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
|
-
**
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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()`.
|
|
128
|
-
|
|
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
|
[](https://github.com/wafertools/wafermap/actions/workflows/deploy.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/@wafertools/wafermap)
|
|
7
7
|

|
|
8
|
-

|
|
9
9
|
[](./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 ~
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
546
|
-
|
|
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
|
|
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
|