@wafertools/wafermap 0.30.0 → 0.30.1
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 +13 -3
- package/CHANGELOG.md +175 -0
- package/README.md +3 -3
- package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
- package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
- 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/correlation.js +2 -2
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +1 -0
- package/dist/packages/canvas-adapter/charts/histogram.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/deprecated.d.ts +0 -3
- package/dist/packages/canvas-adapter/deprecated.js +1 -1
- package/dist/packages/canvas-adapter/dieList.js +1 -1
- package/dist/packages/canvas-adapter/exportName.d.ts +68 -0
- package/dist/packages/canvas-adapter/exportName.js +1 -0
- package/dist/packages/canvas-adapter/icons.js +2 -2
- package/dist/packages/canvas-adapter/index.d.ts +2 -1
- package/dist/packages/canvas-adapter/index.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +25 -3
- package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +24 -2
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +21 -6
- package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
- package/dist/packages/canvas-adapter/toCanvas.js +1 -1
- package/dist/packages/canvas-adapter/toolbar.d.ts +6 -0
- package/dist/packages/canvas-adapter/toolbar.js +2 -2
- package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
- package/dist/packages/canvas-adapter/userGuideHtml.js +15 -8
- 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/deprecated.d.ts +8 -12
- package/dist/packages/core/deprecated.js +1 -1
- package/dist/packages/core/index.d.ts +1 -1
- package/dist/packages/core/index.js +1 -1
- package/dist/packages/core/utils.d.ts +12 -0
- package/dist/packages/core/utils.js +1 -1
- package/dist/packages/core/waferLabel.d.ts +12 -2
- package/dist/packages/core/waferLabel.js +1 -1
- package/dist/packages/renderer/binColors.d.ts +38 -0
- package/dist/packages/renderer/binColors.js +1 -1
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.d.ts +30 -5
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/renderer/deprecate.d.ts +7 -0
- package/dist/packages/renderer/deprecate.js +1 -1
- package/dist/packages/renderer/deprecated.d.ts +2 -2
- package/dist/packages/renderer/deprecated.js +1 -1
- package/dist/packages/renderer/index.d.ts +2 -1
- package/dist/packages/renderer/index.js +1 -1
- package/dist/packages/stats/analyzeWaferLot.js +1 -1
- package/dist/packages/stats/analyzeWaferMap.js +1 -1
- package/dist/packages/stats/capability.d.ts +16 -1
- package/dist/packages/stats/capability.js +1 -1
- package/dist/packages/stats/deprecated.d.ts +30 -35
- package/dist/packages/stats/deprecated.js +1 -1
- package/dist/packages/stats/index.d.ts +4 -2
- package/dist/packages/stats/index.js +1 -1
- package/dist/packages/stats/normalizeInput.d.ts +10 -0
- package/dist/packages/stats/normalizeInput.js +1 -0
- package/dist/packages/stats/regions.d.ts +2 -0
- package/dist/packages/stats/regions.js +1 -1
- package/dist/packages/stats/renderFindingsReport.js +1 -1
- package/dist/packages/stats/renderSummaryReport.d.ts +39 -1
- package/dist/packages/stats/renderSummaryReport.js +26 -26
- package/dist/packages/stats/reportHtml.d.ts +0 -1
- package/dist/packages/stats/reportHtml.js +10 -10
- package/dist/packages/stats/summaryFigures.d.ts +47 -0
- package/dist/packages/stats/summaryFigures.js +1 -0
- package/dist/packages/stats/types.d.ts +118 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -84,7 +84,9 @@ or the other low-level pipeline functions: they are deprecated and removed in 0.
|
|
|
84
84
|
and its pass/fail verdict (pass bins, per `passBins`, take green pass colours), so a
|
|
85
85
|
bin is the same colour in every lot. To choose colours, set `BinDef.color` or register
|
|
86
86
|
a palette with `registerBinColorScheme`. Bin maps and value maps have separate
|
|
87
|
-
schemes: `binColorScheme` and `valueColorScheme`.
|
|
87
|
+
schemes: `binColorScheme` and `valueColorScheme`. Need the colours for a surface of
|
|
88
|
+
your own (a table swatch, an export)? Read `controller.getBinColors()` for a live map,
|
|
89
|
+
or `binColorsForMaps(results)` — never a palette lookup of your own.
|
|
88
90
|
- Build once, render many: `buildWaferMap()` handles data + geometry; re-render UI
|
|
89
91
|
changes through the controller's `setOptions()`, not by rebuilding.
|
|
90
92
|
- `result.view` is internal. Use the promoted fields: `result.plotMode`,
|
|
@@ -96,8 +98,8 @@ or the other low-level pipeline functions: they are deprecated and removed in 0.
|
|
|
96
98
|
`w.message` to display, branch on `w.code`. Code that calls a string method on an
|
|
97
99
|
entry — `warnings[0].includes('…')` — is the old shape and will throw.
|
|
98
100
|
- **`summary.findings` is the complete list and contains restatements of the same
|
|
99
|
-
fact.** Building a list for a human to read?
|
|
100
|
-
|
|
101
|
+
fact.** Building a list for a human to read? Pass it through `visibleFindings()`,
|
|
102
|
+
which drops what other findings absorb — do not re-implement that filter.
|
|
101
103
|
Skip that and one edge failure is reported up to three times per region — a hard
|
|
102
104
|
bin row, its soft-bin twin, and the yield row that restates the pass bin. Do NOT
|
|
103
105
|
use `relatedIds` for this; it is a different relationship and some ids it names
|
|
@@ -133,6 +135,14 @@ it is handed, because it has no way to know which tests anyone will look at.
|
|
|
133
135
|
- **A Web Worker buys responsiveness, not speed.** `createWafermapWorker` copies data
|
|
134
136
|
across `postMessage`, so total time goes *up*. Use it when a build would otherwise
|
|
135
137
|
visibly freeze the page, not for small datasets.
|
|
138
|
+
- **Capability, pass rates and region yield come back from the analysis** —
|
|
139
|
+
`stats.capability` (with `computePerTestStats`), `stats.testSpecYield`,
|
|
140
|
+
`stats.testFlagYield`, `stats.functionalYield` and `stats.regionYield`, on wafer and
|
|
141
|
+
lot summaries alike. Do not compute Cp/Cpk or ring yield yourself: the pooled
|
|
142
|
+
within-wafer stddev and per-wafer pass bins are easy to get subtly wrong.
|
|
143
|
+
- **Reports from code: `renderWaferReportHtml(result, summary)` and
|
|
144
|
+
`renderLotReportHtml(results)`** — they take the built maps, so pass bins and ring
|
|
145
|
+
count cannot be wrong. They run in Node.
|
|
136
146
|
- **In a gallery, pass `perWaferSummaries` to `analyzeWaferLot`** so it reuses the
|
|
137
147
|
per-wafer analysis you already ran instead of redoing it.
|
|
138
148
|
|
package/CHANGELOG.md
CHANGED
|
@@ -22,6 +22,181 @@ under `### Breaking`.
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
## [0.30.1] — 2026-09-16
|
|
26
|
+
|
|
27
|
+
### Security
|
|
28
|
+
|
|
29
|
+
- **Names from a data file could run script in a tooltip.** The die tooltip and the Insights
|
|
30
|
+
chart tooltips (correlation, capability, boxplot, trend, histogram, bar and grouped-bar
|
|
31
|
+
charts), plus the legend-row and toolbar tooltips, built HTML from test names, units, bin
|
|
32
|
+
names, wafer labels and metadata values without escaping them. A file with a test named
|
|
33
|
+
`<img src=x onerror=…>` ran that script when a user hovered, in a Tauri, Electron or
|
|
34
|
+
WebView2 host inside the app's own webview. Every such value is now escaped. The escape
|
|
35
|
+
helper, formerly private to the reports, is the one copy for the library (`core/utils.ts`),
|
|
36
|
+
and `tests/htmlEscaping.test.mjs` fails if a tooltip puts a raw label into HTML again.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **Every saved file is named for the data it came from.** Exports were named for their content
|
|
41
|
+
alone — `dies.csv`, `test-values.csv`, `wafermap.png`, `wafer-gallery.png`, a chart's title — so
|
|
42
|
+
the same export from two wafers of a lot saved as `dies.csv` and `dies (1).csv`, with nothing in
|
|
43
|
+
either name to say which wafer it described. Map, gallery and chart PNGs and every CSV export
|
|
44
|
+
are now named `[lot]_[wafer]_[content]`, for example `LOT123_W05_hard-bin.png`,
|
|
45
|
+
`LOT123_W05_die-list.csv` and `LOT123_25-wafers_yield-by-wafer.png` (see API §5.4.5).
|
|
46
|
+
- A part the data doesn't have is left out, never invented. A wafer with no `label` or
|
|
47
|
+
`waferId` gets no wafer part, not a position that could be read as an ID.
|
|
48
|
+
- A gallery writes its lot and wafer counts (`2-lots`, `25-wafers`), and a lot-stacked map
|
|
49
|
+
writes `stacked-N-wafers`. A card or detached window names files for its own wafer.
|
|
50
|
+
- The name is worked out at save time, so it follows `setResult`, `setItems` and plot-mode
|
|
51
|
+
changes.
|
|
52
|
+
- Names are safe on every common filesystem: path and reserved characters are replaced,
|
|
53
|
+
length is limited, and Windows device names are avoided.
|
|
54
|
+
- **`downloadFilename` is unchanged.** When a host sets it, the map or gallery PNG is still
|
|
55
|
+
named `<downloadFilename>.png`. Only its defaults change: without it, the map PNG was
|
|
56
|
+
`wafermap.png` and the gallery PNG `wafer-gallery.png`, and both now get the generated name.
|
|
57
|
+
Gallery cards, which never read it, also get the generated name, as do all CSVs and charts.
|
|
58
|
+
- `onSaveImage` and `onSaveText` receive the generated name as `suggestedName`. A host that
|
|
59
|
+
matched one of the old default names needs to change.
|
|
60
|
+
- The die-list export's content part is `die-list` (was `dies`).
|
|
61
|
+
- **A floating window's minimize button is now Collapse, and is hidden while the window is
|
|
62
|
+
maximized.** In a desktop host such as Tauri, where a detached gallery card falls back to an
|
|
63
|
+
in-page window, a maximized window's header sits directly under the app's own title bar. Its
|
|
64
|
+
`_` minimize looked like the OS button beside it but only shrank the window to a title strip.
|
|
65
|
+
The button is now drawn as chevrons, with the tooltip **Collapse** (**Show contents** while
|
|
66
|
+
collapsed), and a maximized window shows only restore and close, like a modal. This applies to
|
|
67
|
+
the detached-card fallback window and the user guide window. Modals are unchanged, and
|
|
68
|
+
`ICONS.windowMinimize`/`windowRestore` are still exported.
|
|
69
|
+
- **The data layer is about 3 KB larger (~52 KB gzip).** Analysis now returns capability, pass
|
|
70
|
+
rates by recorded verdict and region yield (see Added), so it includes the code that computes
|
|
71
|
+
them.
|
|
72
|
+
|
|
73
|
+
### Deprecated
|
|
74
|
+
|
|
75
|
+
- **`downloadFilename` (on `RenderOptions` and `GalleryOptions`) becomes a prefix in 0.31.0.** It
|
|
76
|
+
will lead the name of every file a map or gallery saves, CSVs included, with the lot, wafer and
|
|
77
|
+
content appended, for example `LOT123_sort_W05_hard-bin.png`. Parts it already names won't be
|
|
78
|
+
repeated, so a host passing a source-file stem that contains the lot won't see the lot twice.
|
|
79
|
+
It is not being removed. Until 0.31.0 it keeps its current meaning, and passing it logs a
|
|
80
|
+
one-time console notice. If you rely on setting a map's whole file name, say so at
|
|
81
|
+
https://github.com/wafertools/wafermap/issues.
|
|
82
|
+
- **Five 0.30.0 deprecations are withdrawn on review**, as each is the only supported path to
|
|
83
|
+
something a host needs: `visibleFindings` (the one rule for collapsing restated findings —
|
|
84
|
+
`summary.findings` is deliberately uncollapsed), `openReportModal` (shows a report when a host
|
|
85
|
+
has installed its own `setReportOpener`), `metadataDisplayValue` (the one rule for a metadata
|
|
86
|
+
value's text in an export), `getReticleCell` (the only source of a die's reticle cell outside
|
|
87
|
+
the tooltip) and `renderFindingsReportHtml` (it takes a summary alone, so it cannot be given
|
|
88
|
+
inconsistent inputs). They log no notice and stay.
|
|
89
|
+
- **Every remaining deprecation notice now names what to use instead**, or says plainly that
|
|
90
|
+
there is no data replacement (the histogram, scatter and correlation builders). The review
|
|
91
|
+
behind each verdict is recorded in the repository's `API_REMOVALS.md`.
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- **A test could not be chosen from the plot-mode menu on a touchscreen.** With more tests than
|
|
96
|
+
fit inline, "Test Value ▶" opens a submenu, and it opened on hover only. On a phone or tablet a
|
|
97
|
+
tap opened the submenu and the same tap closed it again, so the list flashed and no test could
|
|
98
|
+
be picked. From the keyboard, Enter on the row did nothing. The row now also opens the submenu
|
|
99
|
+
when tapped or activated with Enter or Space. Focus moves into the submenu when it is opened from
|
|
100
|
+
the keyboard, the arrow keys move through its tests, and Escape closes it. Tapping the submenu's
|
|
101
|
+
filter box no longer closes the menu underneath. Applies to both `renderWaferMap` and
|
|
102
|
+
`renderWaferGallery`. Guarded by `tests/modeMenuCascade.test.mjs`.
|
|
103
|
+
- **Map lines and markers got thinner as display scaling rose.** Ring, quadrant and reticle
|
|
104
|
+
lines, the wafer outline, the probe path, the +X/+Y indicator, out-of-spec triangles and the
|
|
105
|
+
failing-die hatch were sized in device pixels, so on a 2× display they drew at half their
|
|
106
|
+
intended size and on a 3× display at a third. The ring and quadrant lines' light centre fell
|
|
107
|
+
below one CSS pixel and faded to a faint hairline. They are now sized in CSS pixels, like the
|
|
108
|
+
colorbar's limit markers, so they look the same at every scale and are sharper at higher ones.
|
|
109
|
+
A 1× display draws exactly as before. The die outline stays at its device-pixel width, so
|
|
110
|
+
dense maps on high-DPI screens are not greyed out. Guarded by `tests/strokeScale.test.mjs`.
|
|
111
|
+
- **Outlined buttons lost their border in a dark theme.** Summary report, View die list and the
|
|
112
|
+
Test values / Functional CSV buttons drew a fixed dark edge (`rgba(0,0,0,0.30)`) whatever the
|
|
113
|
+
theme, so on a dark panel they had no visible outline while the ring/quadrant toggles beside
|
|
114
|
+
them did. `--wmap-control-border` now falls back to `--wmap-border` — the edge the
|
|
115
|
+
toggles use — so a theme that sets `--wmap-border` gets the same outline on both. With neither
|
|
116
|
+
set the default is unchanged, and a host that sets `--wmap-control-border` is unaffected.
|
|
117
|
+
- **Bins and test values given as text built a wrong map without a warning.** A CSV parser gives
|
|
118
|
+
every field as a string. String `x`/`y` already made `buildWaferMap` throw, but a bin of `"1"`
|
|
119
|
+
is not pass bin `1`, so those dies counted as fails and yield read 0 %, and a test value of
|
|
120
|
+
`"0.5"` was not plotted or analysed as a number — with nothing to say so. `buildWaferMap` now
|
|
121
|
+
raises an `input-values-not-numbers` warning (severity `error`) counting text bins, text test
|
|
122
|
+
values and non-boolean verdicts, with an example. The values are not converted: convert them
|
|
123
|
+
and rebuild.
|
|
124
|
+
- **Changing the bin palette inside an expanded gallery card did not recolour the map.** A card
|
|
125
|
+
keeps the gallery-wide bin colours so every card agrees, and those took precedence over the
|
|
126
|
+
palette chosen in the card's own menu, so the menu appeared to do nothing — most visibly for soft
|
|
127
|
+
bins. A palette or "Use colours from bin definitions" change made in the card now resolves the
|
|
128
|
+
card's colours from that choice; a change made in the gallery still recolours every card
|
|
129
|
+
together.
|
|
130
|
+
- **A card reattached to the gallery kept the view it had while expanded.** Reattaching stored the
|
|
131
|
+
expanded window's options — plot mode, palettes, overlays — as that card's per-card overrides,
|
|
132
|
+
which win every time the card is built. A card switched to soft bins while expanded stayed a
|
|
133
|
+
soft-bin map under a gallery bar and legend strip describing hard bins, and later gallery palette
|
|
134
|
+
changes left it in colours matching no other card. A reattached card now takes the gallery's
|
|
135
|
+
shared options again, like every other card. This also stops reattaching from discarding
|
|
136
|
+
per-card `viewOptions` the host set on the item.
|
|
137
|
+
- **A lot report built for wafers without a precomputed summary analysed empty wafers.** The
|
|
138
|
+
report ran its lot analysis on `{ label, wafer, dies, passBins }` pieces, which the analysis
|
|
139
|
+
does not recognise as built maps, so it treated each as a fresh input with no results. The
|
|
140
|
+
report then had no findings, `N/A` per-wafer yields, and a title of "Summary — N wafers"
|
|
141
|
+
instead of "Lot Summary — <lot>". It now analyses the built maps (`renderLotReportHtml`, and
|
|
142
|
+
the gallery's and lot panel's report buttons, which use it).
|
|
143
|
+
- **A gallery with a fixed column count left most of the row empty.** Picking 2 columns drew two
|
|
144
|
+
480px cards and left the rest of the row empty, because a fixed column count was still capped
|
|
145
|
+
by die density (since 0.21.1). A fixed column count now divides the full width; Auto keeps the
|
|
146
|
+
cap.
|
|
147
|
+
- **`GalleryOptions.columns` was ignored at mount**, so the grid always drew a single column.
|
|
148
|
+
Invalid counts (`0`, negative, `NaN`) now fall back to automatic layout, and fractions round.
|
|
149
|
+
|
|
150
|
+
### Added
|
|
151
|
+
|
|
152
|
+
Supported replacements for exports deprecated in 0.30.0 that had no other path. Each deprecated
|
|
153
|
+
export's notice names its replacement; the old names are still removed in 0.31.0.
|
|
154
|
+
|
|
155
|
+
- **Capability, pass rates by recorded verdict, and region yield in the analysis output**
|
|
156
|
+
(API §7.4.1), replacing `buildCapabilityData`, `buildTestPassRateData`/`hasJudgeableTests`
|
|
157
|
+
and `buildRegionYieldData` with the ring/quadrant builders:
|
|
158
|
+
- `stats.capability` — Cp/Cpk/Pp/Ppk per parametric test, computed when per-test statistics
|
|
159
|
+
are (`computePerTestStats`). On a lot summary each wafer is a subgroup, so `stdWithin` is the
|
|
160
|
+
pooled within-wafer stddev and Cp and Pp differ. The chart's normalised five-number fields
|
|
161
|
+
are not included; they are not the test's real minimum and quartiles.
|
|
162
|
+
- `stats.testFlagYield` — per-test pass rate by the tester's recorded verdict — and
|
|
163
|
+
`stats.specVerdictDisagreementDies`, the dies where that verdict and the spec-limit judgement
|
|
164
|
+
disagree. They complement the existing `testSpecYield` and `functionalYield`.
|
|
165
|
+
- `stats.regionYield` — `{ ring, quadrant }` yield with each region's die and pass counts, each
|
|
166
|
+
die judged by its own wafer's pass bins.
|
|
167
|
+
- On `LotStatsSummary.stats`: `capability`, `regionYield`, `testSpecYield`, `functionalYield`,
|
|
168
|
+
`testFlagYield` and `specVerdictDisagreementDies`, all pooled exactly from the wafer summaries
|
|
169
|
+
— counts summed, and capability from each wafer's moments — so a lot analysis given
|
|
170
|
+
`perWaferSummaries` stays cheap. A pooled figure is absent unless every wafer reported it. Lot ring yield is also absent when wafers
|
|
171
|
+
were built with different ring counts.
|
|
172
|
+
- `analyzeWaferLot` now builds a raw `WaferMapInput` once and passes the built map to each
|
|
173
|
+
wafer's analysis.
|
|
174
|
+
- **`renderWaferReportHtml(result, summary?)` and `renderLotReportHtml(results)`**, replacing
|
|
175
|
+
`renderSummaryReportHtml` and `renderLotSummaryReportHtml`. They take built maps, so pass bins
|
|
176
|
+
and ring count come from the map rather than defaulting to `[1]` and `4`. They need no DOM.
|
|
177
|
+
The Summary panel's report buttons use them.
|
|
178
|
+
- **`binColorsForMaps(results, options?)` and `getBinColors()` on `WaferMapController` and
|
|
179
|
+
`GalleryController`** — the colours the maps draw, for a host's own table, export or chart —
|
|
180
|
+
replacing `resolveBinColors` and `getBinColorScheme`. `binColorsForMaps` judges each map by its
|
|
181
|
+
own pass bins; the gallery's own colouring now goes through the same function.
|
|
182
|
+
- **`buildWaferMap({ layout: true, waferConfig, dieConfig })`** — a die layout with no test data:
|
|
183
|
+
every site fully on the wafer (the gross die count), die `(0, 0)` at the centre, counted in the
|
|
184
|
+
configured axis directions. It replaces `createWafer` + `generateDies` + `clipDiesToWafer`, and
|
|
185
|
+
renders like any other result.
|
|
186
|
+
- **`stats.spatialPattern` on `analyzeWaferMap`'s result** — the spatial pattern classifier's label,
|
|
187
|
+
confidence and geometry features (failure density overall and at the edge, the failing
|
|
188
|
+
cluster's radial position, eccentricity, linearity, …), replacing a direct `classifyPattern`
|
|
189
|
+
call. It is present for every wafer the classifier can measure, including those labelled
|
|
190
|
+
`'random'` or `'none'`, which raise no finding, so the features can feed a model of your own with
|
|
191
|
+
negative examples as well as patterned ones.
|
|
192
|
+
- **`ICONS.collapse` and `ICONS.uncollapse`**, the floating-window header's collapse and
|
|
193
|
+
show-contents icons.
|
|
194
|
+
- **`WaferMapController.closeSummaryPanel()` and `GalleryController.setColumns()` are restored.**
|
|
195
|
+
Both were removed in 0.30.0 without a deprecation period. A host needs the first to get the
|
|
196
|
+
map's full width back, for example when loading a new file, and the second to apply its own
|
|
197
|
+
column control or a saved preference without rebuilding the gallery. Closing the panel no
|
|
198
|
+
longer resets the Summary button's notable-findings colour, as the 0.29 version did.
|
|
199
|
+
|
|
25
200
|
## [0.30.0] — 2026-09-15
|
|
26
201
|
|
|
27
202
|
### Breaking
|
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 ~53 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
|
|
|
@@ -72,7 +72,7 @@ export. Add `analyzeWaferMap` for the findings and summary panel, swap in
|
|
|
72
72
|
|
|
73
73
|
The API reference is long because it documents every option, not because you need
|
|
74
74
|
them: [tsmap](https://github.com/wafertools/tsmap), a complete cross-platform desktop
|
|
75
|
-
application built on this library, imports **
|
|
75
|
+
application built on this library, imports **15** of its ~100 exports. Read the
|
|
76
76
|
[Quick Start](https://wafertools.github.io/wafermap/quickstart/) first and treat the
|
|
77
77
|
[API reference](https://wafertools.github.io/wafermap/api/) as something to search,
|
|
78
78
|
not to read.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{SPACE as
|
|
1
|
+
import{SPACE as nt,fontPx as ot,FONT as lt,CLR as it}from"../toolbar.js";import{cardShell as rt,formatValue as st,observeResize as ct,makeTooltip as at,positionChartTooltip as ft,makeBackButton as ht,makeSegmented as dt,growCardToFitContent as ut,resolveChartCanvasColors as mt,PADDING as f,VALUE_WIDTH as z,prepareCanvas as xt}from"./chartShell.js";import{escHtml as _}from"../../core/utils.js";const h=24,m=5,bt=15,N=110,pt=12;export function renderBarPanel(i,U){const{barColor:P,valueLabel:G}=i;let b=i.title,l=i.data;const{card:r,heading:R,controlsRow:I,body:A}=rt(b,U,i.ownerDocument);if(i.selfControl){const e=i.selfControl;I.appendChild(dt(e.options,e.current,n=>q(n),r.ownerDocument))}const{drill:d}=i;let y=!1,B=null;const L=r.ownerDocument.createElement("div");Object.assign(L.style,{color:it.label,fontSize:lt.body,marginBottom:nt.sm}),r.insertBefore(L,A);function D(){const e=[];i.onOpen&&e.push("click to open this wafer"),d&&!y&&e.push(`click a ${d.groupLabelText} to see it by wafer`);const n=e.join(", or ");let o=n?`${n[0].toUpperCase()}${n.slice(1)}.`:"";const t=i.reference?.(l);t&&(o=o?`${o} Dashed line: ${t.label}.`:`Dashed line: ${t.label}.`),L.textContent=o}D();const w=r.ownerDocument.createElement("div"),v=()=>i.reference?bt:0,E=()=>f*2+v()+Math.min(l.length,pt)*(h+m);Object.assign(w.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${E()}px`,scrollbarGutter:"stable"}),A.appendChild(w);const a=r.ownerDocument.createElement("canvas");a.style.display="block",a.style.cursor="default",w.appendChild(a);const k=at(r);let x=-1,M=Math.max(1,...l.map(e=>e.value));const Y=e=>G?G(e):`${st(e.value)} (${e.percent.toFixed(1)}%)`;function F(e){const n=f+v()+e*(h+m),o=f+N,t=a.clientWidth-o-z-f;return{y:n,barX:o,barMaxWidth:Math.max(10,t)}}function p(){w.style.maxHeight=`${E()}px`,ut(r,A,E());const e=w.clientWidth,n=f*2+v()+l.length*(h+m),o=xt(a,r,e,n);if(!o)return;const{ctx:t}=o;t.font=`${ot(-1)}px system-ui, sans-serif`,t.textBaseline="middle";const s=mt(r);l.forEach((c,C)=>{const{y:u,barX:T,barMaxWidth:W}=F(C);C===x&&(t.fillStyle=s.bgHover,t.fillRect(0,u-m/2,e,h+m)),t.fillStyle=s.text,t.textAlign="right";const H=c.label.length>16?`${c.label.slice(0,15)}\u2026`:c.label;t.fillText(H,f+N-8,u+h/2),t.fillStyle=s.track,t.fillRect(T,u,W,h);const S=Math.max(1,c.value/M*W);t.fillStyle=P?P(c,C):s.text,t.fillRect(T,u,S,h),t.fillStyle=s.text,t.textAlign="right",t.fillText(Y(c),T+W+z,u+h/2)});const g=i.reference?.(l);if(g&&l.length){const{barX:c,barMaxWidth:C}=F(0),u=Math.round(c+Math.max(0,Math.min(1,g.value/M))*C)+.5,T=f+v()-3,W=f+v()+l.length*(h+m)-m,H=(Z,tt,et)=>{t.save(),t.strokeStyle=Z,t.lineWidth=tt,t.setLineDash(et),t.beginPath(),t.moveTo(u,T),t.lineTo(u,W),t.stroke(),t.restore()};H(s.bg,3.5,[]),H(s.text,1.5,[4,3]),t.save(),t.font="600 10px system-ui, sans-serif",t.textBaseline="middle",t.textAlign="left";const S=4,O=t.measureText(g.label).width+S*2,X=13,j=f+v()-X-1;let $=u-O/2;$=Math.max(c,Math.min($,c+C-O)),t.fillStyle=s.bg,t.strokeStyle=s.border,t.lineWidth=1,t.beginPath(),t.rect($,j,O,X),t.fill(),t.stroke(),t.fillStyle=s.text,t.fillText(g.label,$+S,j+X/2+.5),t.restore()}}function q(e){if(!i.selfControl)return;i.selfControl.current=e;const n=i.selfControl.onChange(e);l=n.data,M=Math.max(1,...l.map(o=>o.value)),x=-1,n.title&&(b=n.title,R.textContent=b),D(),p()}function J(e){if(!d)return;const n=d.onOpenGroup(e);l=n.data,b=n.title,R.textContent=b,M=Math.max(1,...l.map(o=>o.value)),x=-1,y||(y=!0,B=ht(()=>K(),r.ownerDocument),I.appendChild(B)),D(),p()}function K(){if(!d)return;const e=d.onBack();l=e.data,b=e.title,R.textContent=b,M=Math.max(1,...l.map(n=>n.value)),x=-1,y=!1,B?.remove(),B=null,D(),p()}function V(e){const n=Math.floor((e-f+m/2)/(h+m));return n>=0&&n<l.length?n:-1}a.addEventListener("mousemove",e=>{const n=a.getBoundingClientRect(),o=V(e.clientY-n.top),t=o>=0&&d&&!y&&l[o].itemCount>1,s=o>=0&&(t||!!i.onOpen&&o>=0&&!t);if(o!==x&&(x=o,a.style.cursor=s?"pointer":"default",p()),o>=0){const g=l[o],c=t?`<br><em>click to see this ${_(d.groupLabelText)} by wafer</em>`:i.onOpen?"<br><em>click to open this wafer</em>":"";k.innerHTML=`<strong>${_(g.label)}</strong><br>${_(Y(g))}${c}`,k.style.display="block",ft(k,r,e.clientX,e.clientY)}else k.style.display="none"}),a.addEventListener("mouseleave",()=>{x!==-1&&(x=-1,p()),k.style.display="none"}),a.addEventListener("click",e=>{const n=a.getBoundingClientRect(),o=V(e.clientY-n.top);if(o===-1)return;const t=l[o];if(d&&!y&&t.itemCount>1){J(t);return}i.onOpen?.(t)});const Q=ct(r,()=>p());return p(),{card:r,destroy:()=>Q.disconnect()}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildBinClusterData as
|
|
1
|
+
import{buildBinClusterData as H}from"../../stats/binPareto.js";import{SPACE as T,FONT as w,CLR as E}from"../toolbar.js";import{cardShell as L,makeTooltip as $,makeSegmented as D,renderEmptyState as I}from"./chartShell.js";import{renderGroupedBarPlot as M}from"./groupedBarPlot.js";import{escHtml as g}from"../../core/utils.js";const R=90,_=8;export function renderBinClusterPanel(s){const{groups:f,onSaveImage:h}=s;let n="hbin",c=s.title??"Hard bin pareto";const{card:o,heading:S,body:r,controlsRow:y}=L(c,h,s.ownerDocument);y.appendChild(D([["hbin","Hard bins"],["sbin","Soft bins"]],n,e=>{n=e,c=`${n==="hbin"?"Hard":"Soft"} bin pareto`,S.textContent=c,m()},o.ownerDocument));const u=o.ownerDocument.createElement("div");u.textContent="One cluster per bin \xB7 a sub-bar per group",Object.assign(u.style,{color:E.label,fontSize:w.body,marginBottom:T.sm}),o.insertBefore(u,r);const B=$(o);let l=null;function m(){l?.destroy(),l=null,r.innerHTML="";const e=H(f,n);if(e.bins.length===0||e.groups.length===0){I(r,"No bin data available for the current grouping.");return}const p=e.groups,b=e.bins,C=Math.max(1,...b.flatMap(t=>t.counts));l=M(o,r,B,{rows:b.map(t=>({label:t.label,bars:t.counts.map(a=>({fraction:a/C})),trailing:`${t.total}`})),groups:p,labelWidth:R,maxVisibleRows:_,tooltipHtml:(t,a)=>{const i=b[t],d=i.counts[a],x=i.total>0?d/i.total*100:0;return`<strong>${g(i.label)}</strong> \xB7 ${g(String(p[a]))}<br>${d} dies (${x.toFixed(1)}% of bin)`}})}return m(),{card:o,destroy:()=>l?.destroy()}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildTestBoxplotData as
|
|
1
|
+
import{buildTestBoxplotData as Ze}from"../../stats/boxplot.js";import{SPACE as De,fontPx as N,FONT as et,CLR as tt}from"../toolbar.js";import{fmt as xe}from"../../renderer/fmt.js";import{QUANTITY as Ie}from"./palette.js";import{cardShell as nt,observeResize as ot,makeTooltip as it,positionChartTooltip as lt,makeBackButton as st,makeLinkedTestSelect as at,makeToggle as rt,makeLinkedAxisPrefs as ct,renderEmptyState as He,growCardToFitContent as ft,resolveChartCanvasColors as mt,makeAxisFormat as ut,resolveAxisRange as dt,shouldIncludeLimitsByDefault as xt,drawOffAxisLimits as pt,limitLabelSide as ht,PADDING as L,VALUE_WIDTH as gt,prepareCanvas as bt}from"./chartShell.js";import{escHtml as M}from"../../core/utils.js";const k=24,C=5,Re=110,Tt=12,Oe=20;export function renderBoxplotPanel(h){const{title:Z="Test value distribution",items:We,testDefs:pe,onSaveImage:Ee,groups:u,groupLabelText:ee="group",onOpen:q}=h,Pe=h.openActionLabel??"open that wafer",_e=h.openActionLabel??"open this wafer",{card:a,heading:Ge,body:d,controlsRow:Y}=nt(Z,Ee,h.ownerDocument);a.style.flex="0 0 auto",d.style.flex="0 0 auto",a.style.alignSelf="start";const te=pe.filter(n=>n.testNumber!==void 0);let v=h.selectedTestNumber??te[0]?.testNumber??null,ne=!1,Q=0,x=null,H=null,oe=null,he=null;function Xe(){const n=!u||u.length===0?"":x??"\0overview";if(oe&&he===n)return oe;const m=!u||u.length===0?We:x!==null?u.find(g=>g.key===x)?.items??[]:u.map(g=>({label:g.key,dies:g.items.flatMap(c=>c.dies??[])}));return oe=m,he=n,m}function ge(){x!==null&&!H?(H=st(()=>{be(null),h.onGroupChange?.(null)},a.ownerDocument),Y.appendChild(H)):x===null&&H&&(H.remove(),H=null),Ge.textContent=x!==null?`${Z} \u2014 ${ee}: ${x}`:Z}function be(n){return x===n||n!==null&&!(u??[]).some(m=>m.key===n)?!1:(x=n,ge(),U(),S(),!0)}const Te=at(te,v,n=>{v=n,S(),h.onTestChange?.(n)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Ne=Te.el;Y.appendChild(Ne),Y.appendChild(rt("Log scale",ne,n=>{ne=n,S()},a.ownerDocument));const ie=a.ownerDocument.createElement("span");Object.assign(ie.style,{display:"inline-flex",gap:De.lg,alignItems:"center"}),Y.appendChild(ie);const le=ct(ie,h.axisPrefs,n=>{h.onAxisPrefsChange?.(n),S()},a.ownerDocument),se=a.ownerDocument.createElement("div");Object.assign(se.style,{color:tt.label,fontSize:et.body,marginBottom:De.sm}),a.insertBefore(se,d);function U(){const n=!!u&&u.length>0&&x===null,m=[];n?m.push(`click a ${ee}'s box to see it by wafer`):q&&m.push(`click a box to ${Pe}`);const g=m.length?`${m[0][0].toUpperCase()}${m[0].slice(1)} \xB7 `:"";se.textContent=`${g}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(Q?` \xB7 axis clipped, ${Q} value${Q===1?"":"s"} outside`:"")}U(),ge();const O=it(a);let ae=null;function S(){const{includeLimits:n,clipOutliers:m}=le.get();if(U(),d.innerHTML="",te.length===0||v===null){He(d,"No parametric test data available for box plots.");return}const g=Xe(),c=Ze(g,v),re=pe.find(t=>t.testNumber===v),z=re?.unit,F=re?.limitLow,V=re?.limitHigh;if(c.every(t=>t.count===0)){He(d,"No parametric test data available for box plots.");return}const p=a.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",d.appendChild(p);const ye=L*2+Math.min(c.length,Tt)*(k+C)+Oe;d.style.maxHeight=`${ye}px`,d.style.overflowY="auto",d.style.scrollbarGutter="stable";let W=-1;const ce=c.filter(t=>t.count>0),ve=Math.min(...ce.map(t=>t.min)),we=Math.max(...ce.map(t=>t.max)),Le=n??xt(ve,we,F,V);le.sync(Le,F!==void 0||V!==void 0);const E=dt({dataMin:ve,dataMax:we,limitLow:F,limitHigh:V,includeLimits:Le,clipOutliers:m,values:ce.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});Q=E.clippedCount,U();const R=E.lo,fe=E.hi,Me=fe-R||1,j=ne&&R>0,me=j?Math.log10(R):0,ke=(j?Math.log10(fe):0)-me||1,ue=ut(Math.max(Math.abs(R),Math.abs(fe)),z);function Qe(){const t=L+Re,i=p.clientWidth-t-gt-L;return{plotX:t,plotMaxWidth:Math.max(10,i)}}function A(t,i,s){return j?i+(Math.log10(t)-me)/ke*s:i+(t-R)/Me*s}function Ue(){const t=[];for(let i=0;i<=4;i++)t.push(j?Math.pow(10,me+ke*i/4):R+Me*i/4);return t}function K(){const t=mt(a),i=d.clientWidth,s=L*2+c.length*(k+C)+Oe,$=bt(p,a,i,s);if(!$)return;const{ctx:e}=$;e.font=`${N(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:f}=Qe();c.forEach((o,b)=>{const y=L+b*(k+C),r=y+k/2;b===W&&(e.fillStyle=t.bgHover,e.fillRect(0,y-C/2,i,k+C)),e.fillStyle=t.text,e.textAlign="right";const Fe=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(Fe,L+Re-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const Ve=l+f,_=Je=>Math.min(Ve,Math.max(l,Je)),Ae=A(o.min,l,f),$e=A(o.max,l,f),B=_(Ae),G=_(A(o.q1,l,f)),Be=_(A(o.median,l,f)),de=_(A(o.q3,l,f)),D=_($e),je=Ae<B,Ke=$e>D,I=y+3,X=y+k-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(B,r),e.lineTo(G,r),e.moveTo(de,r),e.lineTo(D,r);const w=3;je?(e.moveTo(B+w,r-w),e.lineTo(B,r),e.lineTo(B+w,r+w)):(e.moveTo(B,I),e.lineTo(B,X)),Ke?(e.moveTo(D-w,r-w),e.lineTo(D,r),e.lineTo(D-w,r+w)):(e.moveTo(D,I),e.lineTo(D,X)),e.stroke(),e.fillStyle=Ie,e.globalAlpha=.35,e.fillRect(G,I,Math.max(1,de-G),X-I),e.globalAlpha=1,e.strokeStyle=Ie,e.strokeRect(G,I,Math.max(1,de-G),X-I),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Be,I),e.lineTo(Be,X),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(xe(o.median,z,"engineering"),l+f+10,r)});const T=L+c.length*(k+C);e.font=`${N(-1)}px system-ui, sans-serif`,pt(e,E.offAxis,{left:l,right:l+f,top:0,bottom:T},"horizontal",t.limitLine,o=>xe(o,z,"engineering"));const ze=new Set(E.offAxis.map(o=>o.value));for(const[o,b]of[[F,"LSL"],[V,"USL"]]){if(o===void 0||ze.has(o))continue;const y=A(o,l,f);e.strokeStyle=t.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(y,0),e.lineTo(y,T),e.stroke(),e.setLineDash([]),e.fillStyle=t.limitLine;const r=ht(y,e.measureText(b).width,l,l+f,b==="LSL");e.textAlign=r<0?"right":"left",e.textBaseline="bottom",e.fillText(b,y+r*3,T-1)}e.font=`${N(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,T),e.lineTo(l+f,T),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";for(const o of Ue()){const b=A(o,l,f);e.beginPath(),e.moveTo(b,T),e.lineTo(b,T+4),e.stroke(),e.fillText(ue.tick(o),b,T+6)}ue.unitLabel&&(e.textAlign="left",e.font=`${N(-1)}px system-ui, sans-serif`,e.fillText(`(${ue.unitLabel})`,l+f+22,T+6),e.font=`${N(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function Ce(t){const i=Math.floor((t-L+C/2)/(k+C));return i>=0&&i<c.length?i:-1}function P(t){return xe(t,z,"engineering")}const J=!!u&&u.length>0&&x===null,Se=t=>!J&&!!q&&g[t]?.waferIndex!==void 0;p.addEventListener("mousemove",t=>{const i=p.getBoundingClientRect(),s=Ce(t.clientY-i.top),$=s>=0&&c[s].count>0&&(J||Se(s));if(s!==W&&(W=s,p.style.cursor=$?"pointer":"default",K()),s>=0&&c[s].count>0){const e=c[s],l=J?`<br><em>click to see this ${M(ee)} by wafer</em>`:Se(s)?`<br><em>click to ${M(_e)}</em>`:"";O.innerHTML=`<strong>${M(e.label)}</strong> (${e.count} dies)<br>max ${M(P(e.max))}<br>q3 ${M(P(e.q3))}<br>median ${M(P(e.median))}<br>q1 ${M(P(e.q1))}<br>min ${M(P(e.min))}${l}`,O.style.display="block",lt(O,a,t.clientX,t.clientY)}else O.style.display="none"}),p.addEventListener("mouseleave",()=>{W!==-1&&(W=-1,K()),O.style.display="none"}),p.addEventListener("click",t=>{const i=p.getBoundingClientRect(),s=Ce(t.clientY-i.top);if(s===-1||c[s].count===0)return;if(J){const e=c[s].label;be(e),h.onGroupChange?.(e);return}const $=g[s]?.waferIndex;q&&$!==void 0&&v!==null&&q($,v)}),ae?.disconnect(),ae=ot(a,()=>K()),K(),ft(a,d,ye)}S();function qe(n){Te.set(n)&&(v=n,S())}function Ye(n){le.set(n)&&S()}return{card:a,setTest:qe,setAxisPrefs:Ye,destroy:()=>ae?.disconnect()}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildCapabilityData as
|
|
1
|
+
import{buildCapabilityData as ue}from"../../stats/capability.js";import{capabilityColor as B}from"./palette.js";import{LEADING as be,SPACE as g,fontPx as G,FONT as Q,CLR as H}from"../toolbar.js";import{cardShell as ye,chartFillHeight as xe,applyCanvasFlow as ge,observeResize as ve,makeTooltip as Se,positionChartTooltip as Me,renderEmptyState as Te,resolveChartCanvasColors as Le,chartSwatchCss as ke,prepareCanvas as Ce,chartDpr as Pe}from"./chartShell.js";import{fmt as $e}from"../../renderer/fmt.js";import{escHtml as ee}from"../../core/utils.js";const Ae=30,We=160,te=90,ne=28,v=42;function I(C){return C===null?"\u2014":C.toFixed(2)}export function renderCapabilityPanel(C){const{title:oe="Process capability",items:le,testDefs:z,onSaveImage:se,onSelectTest:O}=C;let X=C.selectedTestNumber??null;const{card:r,body:S}=ye(oe,se,C.ownerDocument),ie=z.some(n=>n.testNumber!==void 0);S.style.overflowX="auto";function ae(){return le}const P=r.ownerDocument.createElement("div");Object.assign(P.style,{display:"flex",flexDirection:"column",gap:g.xs,marginBottom:g.sm}),r.insertBefore(P,S);const W=Se(r);let R=()=>{};function re(n,i,f){P.innerHTML="";const c=r.ownerDocument.createElement("span");Object.assign(c.style,{display:"inline-flex",alignItems:"center",gap:g.sm,color:H.value,fontSize:Q.body,fontWeight:"500"});const M=f-n,b=i>0?` \xB7 ${i} without spec limits`:"",y=r.ownerDocument.createElement("span");y.textContent=M>0?`${n} of ${f} tests shown${b} \xB7 ${M} excluded (no recorded values)`:`${n} test${n!==1?"s":""} shown${b}`,c.appendChild(y),P.appendChild(c);const $=i>=n,T=r.ownerDocument.createElement("div");T.textContent=$?"No test has both spec limits, so each is normalised to its own observed range (min = 0, max = 1) and sorted most-variable first. Ppk needs limits, so none is shown.":"Normalised to spec limits (LSL = 0, USL = 1), worst Ppk first. Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(T.style,{color:H.label,fontSize:Q.body,lineHeight:be.base,marginTop:g.xs,maxWidth:"78ch"}),P.appendChild(T),P.appendChild(ce(r.ownerDocument,i>0,$))}function ce(n,i,f=!1){const c=n.createElement("div");Object.assign(c.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${g.xs} ${g.xl}`,marginTop:g.xl,marginBottom:g.xs,fontSize:Q.body,color:H.label});const M=f?[]:[{color:B(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:B(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:B(.5),label:"Poor \xB7 Ppk < 1.0"}];i&&M.push({color:H.label,label:"No spec limits \xB7 no Ppk",dashed:!0});for(const b of M){const y=n.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:g.sm});const $=n.createElement("span");$.style.cssText=ke(b.color,b.dashed?"outline":"solid");const T=n.createElement("span");T.textContent=b.label,y.append($,T),c.appendChild(y)}return c}function me(n){if(S.innerHTML="",n.length===0)return Te(S,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const i=r.ownerDocument.createElement("canvas");i.style.display="block",i.style.cursor="default",S.appendChild(i);const f=n.length,c=Math.min(0,...n.map(t=>t.min)),M=Math.max(1,...n.map(t=>t.max)),b=(M-c)*.05||.1,y=c-b,T=M+b-y||1;function V(t){return Math.max(Ae,Math.min(We,Math.floor(t/f)))}function L(t,s,o){return s+(1-(t-y)/T)*o}function he(t,s,o,l,k){const x=n.filter(a=>a.hasSpec).length,A=x===n.length,d=x===0,_=d?"max":"USL",e=d?"min":"LSL",h=A?"normalised to spec":d?"normalised to range":"normalised (per test)";t.font=`${G(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle";for(const[a,D]of[[1,_],[0,e]]){const p=L(a,o,l);p<o-1||p>o+l+1||(t.strokeStyle=s.limitLine,t.lineWidth=1,t.beginPath(),t.moveTo(v-5,p),t.lineTo(v,p),t.stroke(),t.fillStyle=s.textMuted,t.fillText(D,v-8,p))}t.save(),t.translate(9,o+l/2),t.rotate(-Math.PI/2),t.textAlign="center",t.fillStyle=s.textMuted,t.fillText(h,0,0),t.restore()}let w=-1;function j(){ge(i);const t=S.clientWidth,s=V(t-v),o=s*f,l=ne,k=v+o,x=xe(r,S,i,ne+200+te),A=Math.max(l+60,x-te),d=A-l,_=Ce(i,r,k,x);if(!_)return;const{ctx:e}=_,h=Le(r);n.forEach((a,D)=>{const p=v+D*s,m=p+s/2,u=Math.max(4,s*.55);if(D===w&&(e.fillStyle=h.bgHover,e.fillRect(p,l,s,A-l)),n[D].testNumber===X&&(e.fillStyle=h.accent,e.fillRect(p,A-2,s,2)),a.hasSpec){const K=L(0,l,d),Z=L(1,l,d);e.strokeStyle=h.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(p,K),e.lineTo(p+s,K),e.moveTo(p,Z),e.lineTo(p+s,Z),e.stroke(),e.setLineDash([])}const F=a.hasSpec?B(a.ppk):h.textMuted,U=L(a.min,l,d),E=L(a.q1,l,d),J=L(a.median,l,d),N=L(a.q3,l,d),q=L(a.max,l,d);e.strokeStyle=h.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(m,U),e.lineTo(m,E),e.moveTo(m,N),e.lineTo(m,q),e.moveTo(m-u/4,U),e.lineTo(m+u/4,U),e.moveTo(m-u/4,q),e.lineTo(m+u/4,q),e.stroke(),e.fillStyle=F,e.globalAlpha=.5,e.fillRect(m-u/2,Math.min(E,N),u,Math.max(1,Math.abs(N-E))),e.globalAlpha=1,e.strokeStyle=F,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(m-u/2,Math.min(E,N),u,Math.max(1,Math.abs(N-E))),e.setLineDash([]),e.strokeStyle=h.text,e.lineWidth=2,e.beginPath(),e.moveTo(m-u/2,J),e.lineTo(m+u/2,J),e.stroke(),e.lineWidth=1,e.font=`${G(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?F:h.textMuted,e.fillText(a.hasSpec?I(a.ppk):"\u2014",m,l-8);const fe=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(m,A+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${G(-1)}px system-ui, sans-serif`,e.fillStyle=h.textMuted,e.fillText(fe,0,0),e.restore()}),e.strokeStyle=h.border,e.lineWidth=1,e.strokeRect(v,l,o,d),he(e,h,l,d,o)}function Y(t){const s=i.getBoundingClientRect(),o=S.clientWidth,l=V(o-v),k=(t.clientX-s.left)*(i.width/Pe(i)/s.width)-v,x=Math.floor(k/l);return x>=0&&x<f?x:-1}return i.addEventListener("mousemove",t=>{const s=Y(t);if(s!==w&&(w=s,j()),s===-1){W.style.display="none";return}const o=n[s],l=k=>ee($e(k,o.unit));W.innerHTML=`<strong>${ee(o.label)}</strong> (n=${o.n})<br>`+(o.hasSpec?`LSL ${l(o.lsl)} \xB7 USL ${l(o.usl)}<br>mean ${l(o.mean)}<br>Cp ${I(o.cp)} \xB7 Cpk ${I(o.cpk)}<br>Pp ${I(o.pp)} \xB7 Ppk ${I(o.ppk)}`:`<em>No spec limits \u2014 sorted by variability</em><br>mean ${l(o.mean)} \xB7 stddev ${l(o.stdOverall)}`)+(O?"<br><em>click to view in boxplot</em>":""),W.style.display="block",Me(W,r,t.clientX,t.clientY),i.style.cursor=O?"pointer":"default"}),i.addEventListener("mouseleave",()=>{w!==-1&&(w=-1,j()),W.style.display="none"}),i.addEventListener("click",t=>{if(!O)return;const s=Y(t);s!==-1&&O(n[s].testNumber)}),j}function de(){const n=ue(ae(),z),i=z.filter(c=>c.testNumber!==void 0).length,f=n.filter(c=>!c.hasSpec).length;re(n.length,f,i),R=me(n),R()}const pe=ve(r,()=>R());return de(),{card:r,hasData:ie,setTest:n=>{X!==n&&(X=n,R())},destroy:()=>pe.disconnect()}}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{buildCorrelationMatrix as
|
|
2
|
-
`),"test-correlation.csv","text/csv",x.onSaveText)}),
|
|
1
|
+
import{buildCorrelationMatrix as Mt,filterCorrelationMatrix as wt}from"../../stats/correlation.js";import{csvField as it}from"../../core/utils.js";import{CORRELATION_POSITIVE as at,CORRELATION_NEGATIVE as ct}from"./palette.js";import{buildFacetTable as vt}from"../../stats/facets.js";import{wireControlHover as Lt,controlStyle as Ct,SPACE as C,RADIUS as et,fontPx as dt,FONT as z,CLR as b,saveTextFile as It}from"../toolbar.js";import{attachChartTip as Tt,cardShell as St,observeResize as $t,makeTooltip as Et,positionChartTooltip as Rt,makeWaferSelect as Dt,renderEmptyState as kt,resolveChartCanvasColors as At,prepareCanvas as Bt,chartDpr as ut}from"./chartShell.js";import{escHtml as G}from"../../core/utils.js";const ft=5,pt=100,mt=20;function nt(x){const d=x.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(d)return[+d[1],+d[2],+d[3]];const f=x.trim().replace("#","");return f.length===6?[parseInt(f.slice(0,2),16),parseInt(f.slice(2,4),16),parseInt(f.slice(4,6),16)]:null}function Ot(x){const d=nt(x);return d?(d[0]*299+d[1]*587+d[2]*114)/1e3>140?"#111":"#fff":"#000"}function _t(x,d,f){const B=nt(x);if(!B)return x;const U=Math.round(d[0]+(B[0]-d[0])*f),O=Math.round(d[1]+(B[1]-d[1])*f),l=Math.round(d[2]+(B[2]-d[2])*f);return`rgb(${U},${O},${l})`}export function renderCorrelationPanel(x){const{title:d="Test correlation matrix",items:f,testDefs:B,onSaveImage:U,onSelectPair:O}=x,{card:l,body:D,controlsRow:q}=St(d,U,x.ownerDocument);D.style.overflowX="auto",l.style.alignSelf="start";let Y=mt,j=null;const H=l.ownerDocument.createElement("label");H.textContent="Max tests:",Object.assign(H.style,{color:b.label,fontSize:z.body,display:"flex",alignItems:"center",gap:C.xs});const L=l.ownerDocument.createElement("input");L.type="number",L.min=String(ft),L.max=String(pt),L.value=String(Y),Object.assign(L.style,{width:"52px",fontSize:z.body,padding:`${C.xxs} ${C.xs}`,background:b.menuBg,color:b.value,border:`1px solid ${b.menuBorder}`,borderRadius:et.control}),L.addEventListener("change",()=>{const e=Math.max(ft,Math.min(pt,parseInt(L.value,10)||mt));L.value=String(e),e!==Y&&(Y=e,K())}),H.appendChild(L),q.appendChild(H),f.length>1&&q.appendChild(Dt(f,j,e=>{j=e,K()},{ownerDocument:l.ownerDocument}));function ht(){return j!==null?f[j]?[f[j]]:[]:f}if(x.onSaveText){const e=l.ownerDocument.createElement("button");e.type="button",e.textContent="Correlation CSV",Object.assign(e.style,{...Ct("outlined"),color:b.text}),Lt(e),e.addEventListener("click",()=>{const o=ot;if(!o)return;const u=["Test X,Test X number,Test Y,Test Y number,r,n"];for(let r=0;r<o.tests.length;r++)for(let p=r+1;p<o.tests.length;p++){const i=o.cells.find(m=>m.xIndex===p&&m.yIndex===r);i&&u.push([it(o.tests[p].label),String(o.tests[p].testNumber),it(o.tests[r].label),String(o.tests[r].testNumber),i.r===null?"":i.r.toFixed(6),String(i.n)].join(","))}It(u.join(`
|
|
2
|
+
`),"test-correlation.csv","text/csv",x.onSaveText)}),q.appendChild(e)}const _=l.ownerDocument.createElement("div");Object.assign(_.style,{display:"flex",flexDirection:"column",gap:C.xs,marginBottom:C.sm}),l.insertBefore(_,D);const I=Et(l);Tt(H,l,I,"Cap on how many tests the matrix includes (strongest correlations kept first)");let J=()=>{};function xt(e,o,u,r,p,i){if(_.innerHTML="",p.length>0){const w=l.ownerDocument.createElement("div");w.textContent=`\u26A0 Mixed ${p.join(", ")} within this set \u2014 correlations may be misleading (Simpson's paradox). Use Group by, or the Wafer picker, to narrow to a like-for-like set.`,Object.assign(w.style,{color:b.warnText,background:b.warnBg,border:`1px solid ${b.warnBorder}`,borderRadius:et.control,padding:`${C.xs} ${C.md}`,fontSize:z.body}),_.appendChild(w)}const m=l.ownerDocument.createElement("span");Object.assign(m.style,{display:"inline-flex",alignItems:"center",gap:C.sm,color:b.label,fontSize:z.body,flexWrap:"wrap"});const g=l.ownerDocument.createElement("span");g.textContent=i!==null?`Pearson r \xB7 n \u2248 ${i.toLocaleString()} dies per pair \xB7 click a cell to view that pair in scatter \xB7`:"Pearson r \xB7 click a cell to view that pair in scatter \xB7",m.appendChild(g);const k=l.ownerDocument.createElement("span");Object.assign(k.style,{display:"inline-flex",alignItems:"center",gap:C.xs});const T=l.ownerDocument.createElement("span");T.textContent="\u22121";const N=l.ownerDocument.createElement("span");Object.assign(N.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:et.control,border:`1px solid ${b.menuBorder}`,background:`linear-gradient(to right, ${ct}, ${b.menuBg}, ${at})`});const a=l.ownerDocument.createElement("span");a.textContent="+1",k.append(T,N,a),m.appendChild(k),_.appendChild(m);const M=l.ownerDocument.createElement("span");if(Object.assign(M.style,{color:b.value,fontSize:z.body,fontWeight:"500"}),e===0&&o===0)M.textContent=r?`No significant correlations found \u2014 strongest pair: ${r.xLabel} \u2194 ${r.yLabel} (r = ${r.r.toFixed(2)})`:"No significant correlations found";else{const w=[];e>0&&w.push(`${e} strong (|r| \u2265 0.7)`),o>0&&w.push(`${o} moderate (0.4\u20130.7)`);const F=e+o;let X=w.join(", ")+` pair${F!==1?"s":""} found`;u>0&&(X+=` \xB7 ${u} weak pair${u!==1?"s":""} not shown`),M.textContent=X}_.appendChild(M)}function gt(e){if(D.innerHTML="",e.tests.length<2)return kt(D,"Need at least two parametric tests for a correlation matrix."),()=>{};const o=l.ownerDocument.createElement("canvas");o.style.display="block",o.style.cursor="default",D.appendChild(o);const u=e.tests.length,r=s=>s.label.split(" (#")[0],p=Math.min(14,Math.max(...e.tests.map(s=>r(s).length))),i=p*6.5+8,m=10,g=Math.round(m*6.5*Math.sin(Math.PI/4))+14,k=14,T=28,N=44;let a=-1,M=-1;function w(s){const c=Math.max(0,s-i);return Math.max(k,Math.min(N,Math.floor(c/u)))}const F=new Map;for(const s of e.cells){let c=F.get(s.yIndex);c||(c=[],F.set(s.yIndex,c)),c.push(s)}function X(){const s=At(l),c=nt(s.bg)??[255,255,255],h=D.clientWidth,n=w(h),W=n*u,S=g+n*u+4,$=i+W,E=Bt(o,l,$,S);if(!E)return;const{ctx:t}=E;t.font=`${dt(-1)}px system-ui, sans-serif`,t.fillStyle=s.text,e.tests.forEach((R,y)=>{const v=r(R),A=v.length>m?`${v.slice(0,m-1)}\u2026`:v,Q=i+y*n+n/2;t.save(),t.translate(Q,g-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=y===a?s.text:s.textMuted,t.fillText(A,0,0),t.restore()}),e.tests.forEach((R,y)=>{const v=r(R),A=g+y*n,Q=A+n/2;t.font=`${dt(-1)}px system-ui, sans-serif`,t.textAlign="right",t.textBaseline="middle",t.fillStyle=y===M?s.text:s.textMuted,t.fillText(v.length>p?`${v.slice(0,p-1)}\u2026`:v,i-4,Q),(F.get(y)??[]).forEach(lt=>{const Z=lt.xIndex,P=i+Z*n,yt=Z===a&&y===M,rt=Z===y,V=lt.r;if(V===null){t.fillStyle=s.border,t.globalAlpha=.4,t.fillRect(P+1,A+1,n-2,n-2),t.globalAlpha=1;return}let tt=s.bgHover;rt?t.fillStyle=s.bgHover:(tt=_t(V>=0?at:ct,c,Math.abs(V)),t.fillStyle=tt),t.fillRect(P+1,A+1,n-2,n-2),n>=T&&!rt&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Ot(tt),t.textAlign="center",t.textBaseline="middle",t.fillText(V.toFixed(2),P+n/2,A+n/2)),yt&&(t.strokeStyle=s.text,t.lineWidth=2,t.strokeRect(P+1,A+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=s.bg,t.lineWidth=1;for(let R=0;R<=u;R++){const y=i+R*n;t.beginPath(),t.moveTo(y,g),t.lineTo(y,g+u*n),t.stroke();const v=g+R*n;t.beginPath(),t.moveTo(i,v),t.lineTo(i+u*n,v),t.stroke()}}function st(s){const c=o.getBoundingClientRect(),h=D.clientWidth,n=w(h),W=(s.clientX-c.left)*(o.width/ut(o)/c.width),S=(s.clientY-c.top)*(o.height/ut(o)/c.height),$=Math.floor((W-i)/n),E=Math.floor((S-g)/n);return $>=0&&$<u&&E>=0&&E<u?{xi:$,yi:E}:null}return o.addEventListener("mousemove",s=>{const c=st(s);if(!c){I.style.display="none",o.style.cursor="default";return}const{xi:h,yi:n}=c,W=h===n;o.style.cursor=W||!O?"default":"pointer";const S=e.cells.find(t=>t.xIndex===h&&t.yIndex===n),$=e.tests[h].label,E=e.tests[n].label;W?I.innerHTML=`<strong>${G($)}</strong>`:S?.r!==null&&S?.r!==void 0?I.innerHTML=`<strong>${G(r(e.tests[n]))}</strong> (#${e.tests[n].testNumber}) vs <strong>${G(r(e.tests[h]))}</strong> (#${e.tests[h].testNumber})<br>r = ${S.r.toFixed(4)} \xB7 n = ${S.n.toLocaleString()}${O?"<br><em>click to view in scatter</em>":""}`:I.innerHTML=`${G(`${E} vs ${$}`)}<br><em>insufficient data</em>`,I.style.display="block",Rt(I,l,s.clientX,s.clientY)}),o.addEventListener("mouseleave",()=>{I.style.display="none"}),o.addEventListener("click",s=>{const c=st(s);if(!c||!O)return;const{xi:h,yi:n}=c;h!==n&&(a=h,M=n,X(),O(e.tests[h].testNumber,e.tests[n].testNumber))}),X}let ot=null;function K(){const e=ht(),o=e.flatMap(a=>a.dies??[]),u=Mt(o,B),{matrix:r,strongPairs:p,moderatePairs:i,hiddenWeakPairs:m,strongestPair:g}=wt(u,{minTests:6,maxTests:Y}),k=vt(e,{facetableOnly:!0}).filter(a=>a.splittable).map(a=>a.label),T=r.cells.filter(a=>a.xIndex!==a.yIndex).map(a=>a.n).sort((a,M)=>a-M),N=T.length?T[Math.floor(T.length/2)]:null;ot=r,xt(p,i,m,g,k,N),J=gt(r),J()}const bt=$t(l,()=>J());return K(),{card:l,destroy:()=>bt.disconnect()}}
|
|
@@ -33,6 +33,7 @@ export interface GroupedBarPlotOptions {
|
|
|
33
33
|
/** Sub-bar colour by group index. Defaults to the CVD-safe categorical ramp. */
|
|
34
34
|
colorOf?: (groupIndex: number) => string;
|
|
35
35
|
/** Tooltip HTML for a hovered sub-bar. */
|
|
36
|
+
/** HTML for the hover tooltip. Escape every label with `escHtml` (core/utils.ts): they come from input files. */
|
|
36
37
|
tooltipHtml: (rowIndex: number, groupIndex: number) => string;
|
|
37
38
|
}
|
|
38
39
|
export interface GroupedBarPlotHandle {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildTestHistogramData as jt,collectTestValues as Ht,buildTestHistogramSeries as _t,testValueExtent as At}from"../../stats/histogram.js";import{SPACE as at,fontPx as Q,FONT as Ct,CLR as $t}from"../toolbar.js";import{fmt as bt}from"../../renderer/fmt.js";import{QUANTITY as Ut,categorical as Xt}from"./palette.js";import{cardShell as Yt,observeResize as Pt,makeTooltip as Qt,attachChartTip as qt,positionChartTooltip as Wt,makeLinkedTestSelect as Jt,makeWaferSelect as Zt,makeLinkedAxisPrefs as te,renderEmptyState as Tt,chartFillHeight as Dt,applyCanvasFlow as Et,makeAxisFormat as It,PADDING as ft,robustFence as Bt,shouldIncludeLimitsByDefault as Ft,drawOffAxisLimits as ee,resolveAxisRange as ne,chartSwatchCss as ie,makeSeriesLegendItem as oe,prepareCanvas as Kt}from"./chartShell.js";const St=230,Ot=36,yt=18;function Rt(l,G,q,J,Mt,B,r,y=4){const Z=Math.max(1,B/y),rt=Math.pow(10,Math.floor(Math.log10(Z))),d=Z/rt,tt=(d<=1?1:d<=2?2:d<=5?5:10)*rt,K=J-q;l.save(),l.font=`${Q(-1)}px system-ui, sans-serif`,l.textAlign="right",l.textBaseline="middle";for(let V=0;V<=B+1e-9;V+=tt){const ct=J-V/(B||1)*K;l.strokeStyle=r.grid,l.lineWidth=1,l.beginPath(),l.moveTo(G,ct),l.lineTo(G+Mt,ct),l.stroke(),l.fillStyle=r.text,l.fillText(String(Math.round(V)),G-4,ct)}l.strokeStyle=r.axis,l.lineWidth=1,l.beginPath(),l.moveTo(G,q),l.lineTo(G,J),l.stroke(),l.restore()}export function renderHistogramPanel(l){const{title:G="Test value distribution",items:q,testDefs:J,onSaveImage:Mt,groups:B}=l,{card:r,body:y,controlsRow:Z}=Yt(G,Mt,l.ownerDocument),rt=J.filter(s=>s.testNumber!==void 0);let d=l.selectedTestNumber??rt[0]?.testNumber??null,tt=null,K=0;const V=Jt(rt,d,s=>{d=s,j(),l.onTestChange?.(s)},{maxWidth:"200px",ownerDocument:r.ownerDocument}),ct=V.el;Z.appendChild(ct);const wt=Zt(q,tt,s=>{tt=s,j()},{ownerDocument:r.ownerDocument});Z.appendChild(wt);function Nt(s){return S===s||s!==null&&!(B??[]).some(v=>v.key===s)?!1:(S=s,j(),!0)}const Lt=r.ownerDocument.createElement("span");Object.assign(Lt.style,{display:"inline-flex",gap:at.lg,alignItems:"center"}),Z.appendChild(Lt);const gt=te(Lt,l.axisPrefs,s=>{l.onAxisPrefsChange?.(s),j()},r.ownerDocument),C=Qt(r);let dt=null,S=null;function et(s){const v=J.find(nt=>nt.testNumber===s);return{unit:v?.unit,limitLow:v?.limitLow,limitHigh:v?.limitHigh}}function j(){const{includeLimits:s,clipOutliers:v}=gt.get();if(dt?.disconnect(),dt=null,y.innerHTML="",d===null){Tt(y,"No parametric test data available for a histogram.");return}const nt=!!B&&B.length>0;let it=!1,u;if(nt){const{limitLow:a,limitHigh:m}=et(d),o=B.flatMap(x=>x.items),{min:e,max:p}=At(o,d);if(it=s??Ft(e,p,a,m),v){const x=Ht(o,d),H=Bt(x);H&&(u={lo:Math.max(e,H.lo),hi:Math.min(p,H.hi)},K=x.reduce((lt,E)=>lt+(E<u.lo||E>u.hi?1:0),0))}u||(K=0)}const $=nt?_t(B,d,16,it?et(d).limitLow:void 0,it?et(d).limitHigh:void 0,u):null;if(wt.style.display=$?"none":"",$){const{limitLow:a,limitHigh:m}=et(d);if(gt.sync(it,a!==void 0||m!==void 0),$.series.length===0){Tt(y,"No parametric test data available for a histogram.");return}zt($);return}const _=tt!==null?[q[tt]]:q,{unit:O,limitLow:P,limitHigh:W}=et(d),k=Ht(_,d),{min:ot,max:mt}=At(_,d),ut=s??Ft(ot,mt,P,W);gt.sync(ut,P!==void 0||W!==void 0);const U=v?Bt(k):null,X=U?{lo:Math.max(ot,U.lo),hi:Math.min(mt,U.hi)}:void 0;K=X?k.filter(a=>a<X.lo||a>X.hi).length:0;const h=jt(_,d,16,ut?P:void 0,ut?W:void 0,X);if(h.length===0){Tt(y,"No parametric test data available for a histogram.");return}const D=Math.max(...h.map(a=>a.count),1),Y=r.ownerDocument.createElement("div");Object.assign(Y.style,{fontSize:Ct.body,color:$t.label,marginBottom:at.xxs}),Y.textContent=`max ${D} dies/bucket`+(K?` \xB7 ${K} value${K===1?"":"s"} outside clipped range`:""),y.appendChild(Y);const M=r.ownerDocument.createElement("canvas");M.style.display="block",M.style.cursor="default",y.appendChild(M);let F=-1;const n=h[0].rangeLow,g=h[h.length-1].rangeHigh,L=g-n||1,t=It(Math.max(Math.abs(n),Math.abs(g)),O);function c(a){const m=ft+36,o=M.clientWidth-m-ft,e=a-Ot-yt;return{plotX:m,plotMaxWidth:Math.max(10,o),plotMaxHeight:e,plotTop:yt}}function f(a){const{plotX:m,plotMaxWidth:o}=c(St);if(a<m||a>m+o)return-1;const e=Math.floor((a-m)/o*h.length);return e>=0&&e<h.length?e:-1}function T(){Et(M,Y);const a=y.clientWidth,m=Dt(r,y,M,St),o=Kt(M,r,a,m);if(!o)return;const{ctx:e,theme:p}=o,{plotX:x,plotMaxWidth:H,plotMaxHeight:lt,plotTop:E}=c(m),i=E+lt,w=H/h.length;Rt(e,x,E,i,H,D,{text:p.textMuted,axis:p.border,grid:p.border}),h.forEach((b,z)=>{const I=b.count/D*lt,vt=x+z*w,kt=i-I;e.fillStyle=z===F?p.text:Ut,e.globalAlpha=z===F?1:.7,e.fillRect(vt+1,kt,Math.max(1,w-2),I),e.globalAlpha=1,z===F&&(e.fillStyle=p.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${b.count}`,vt+w/2,kt-2))});const A=b=>x+(b-n)/L*H;e.font=`${Q(-1)}px system-ui, sans-serif`;const{offAxis:ht}=ne({dataMin:n,dataMax:g,limitLow:P,limitHigh:W,includeLimits:!1});ee(e,ht,{left:x,right:x+H,top:E,bottom:i},"horizontal",p.limitLine,t.tick);const pt=new Set(ht.map(b=>b.value));for(const[b,z]of[[P,"LSL"],[W,"USL"]]){if(b===void 0||pt.has(b))continue;const I=A(b);e.strokeStyle=p.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(I,E),e.lineTo(I,i),e.stroke(),e.setLineDash([]),e.fillStyle=p.limitLine,e.textAlign="center",e.textBaseline="bottom",e.fillText(z,I,E-2)}e.font=`${Q(-1)}px system-ui, sans-serif`,e.strokeStyle=p.border,e.lineWidth=1,e.beginPath(),e.moveTo(x,i),e.lineTo(x+H,i),e.stroke(),e.fillStyle=p.textMuted,e.textAlign="center",e.textBaseline="top";const st=Math.max(2,Math.floor(H/44)),R=Math.ceil(h.length/st),N=Math.max(1,R);for(let b=0;b<=h.length;b+=N){const z=b<h.length?h[b].rangeLow:h[h.length-1].rangeHigh,I=x+b*w;e.beginPath(),e.moveTo(I,i),e.lineTo(I,i+4),e.stroke(),e.fillText(t.tick(z),I,i+6)}t.unitLabel&&(e.save(),e.font=`${Q(-1)}px system-ui, sans-serif`,e.fillStyle=p.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${t.unitLabel})`,x+H,i+20),e.restore())}M.addEventListener("mousemove",a=>{const m=M.getBoundingClientRect(),o=f(a.clientX-m.left);if(o!==F&&(F=o,T()),o>=0){const e=h[o];C.innerHTML=`<strong>${bt(e.rangeLow,O,"engineering")} \u2013 ${bt(e.rangeHigh,O,"engineering")}</strong><br>${e.count} dies`,C.style.display="block",Wt(C,r,a.clientX,a.clientY)}else C.style.display="none"}),M.addEventListener("mouseleave",()=>{F!==-1&&(F=-1,T()),C.style.display="none"}),dt=Pt(r,()=>T()),T()}function zt(s){const{unit:v,limitLow:nt,limitHigh:it}=et(d),u=s.ranges,$=s.series,_=Xt;S&&!$.some(n=>n.groupKey===S)&&(S=null);const O=Math.max(1,...$.flatMap(n=>n.counts)),P=r.ownerDocument.createElement("div");Object.assign(P.style,{fontSize:Ct.body,color:$t.label,marginBottom:at.xxs}),P.textContent=`${$.length} groups \xB7 max ${O} dies/bucket`,y.appendChild(P);const W=r.ownerDocument.createElement("div");Object.assign(W.style,{display:"flex",flexWrap:"wrap",gap:at.sm,marginTop:at.md,marginBottom:at.xl}),$.forEach((n,g)=>{const L=oe(n.groupKey,_(g),r.ownerDocument),t=L.el;qt(t,r,C,`${n.groupKey} \u2014 click to emphasize (dim the rest)`),L.setState({selected:S===n.groupKey,dimmed:S!==null&&S!==n.groupKey}),t.addEventListener("click",()=>{const c=S===n.groupKey?null:n.groupKey;Nt(c),l.onGroupChange?.(c)}),W.appendChild(t)}),y.appendChild(W);const k=r.ownerDocument.createElement("canvas");k.style.display="block",k.style.cursor="default",y.appendChild(k);const ot=u[0].rangeLow,mt=u[u.length-1].rangeHigh,ut=mt-ot||1,U=It(Math.max(Math.abs(ot),Math.abs(mt)),v),X=n=>{const g=(n.ownerDocument.defaultView??window).getComputedStyle(n);return n.offsetHeight+(parseFloat(g.marginTop)||0)+(parseFloat(g.marginBottom)||0)},h=()=>X(P)+X(W);let D=-1,Y={plotX:ft+36,plotMaxWidth:10,barWidth:10};function M(){Et(k,h());const n=y.clientWidth,g=Dt(r,y,k,St),L=Kt(k,r,n,g);if(!L)return;const{ctx:t,theme:c}=L,f=ft+36,T=Math.max(10,n-f-ft),a=yt,m=g-Ot-yt,o=a+m,e=T/u.length;Y={plotX:f,plotMaxWidth:T,barWidth:e},Rt(t,f,a,o,T,O,{text:c.textMuted,axis:c.border,grid:c.border}),D>=0&&(t.fillStyle=c.bgHover,t.globalAlpha=.5,t.fillRect(f+D*e,a,e,m),t.globalAlpha=1);const p=$.map((i,w)=>({s:i,i:w})).sort((i,w)=>(i.s.groupKey===S?1:0)-(w.s.groupKey===S?1:0));for(const{s:i,i:w}of p){const A=S===i.groupKey,ht=S!==null&&!A,pt=_(w);t.strokeStyle=pt,t.fillStyle=pt,t.lineWidth=A?2:1,t.globalAlpha=ht?.12:S?.32:.22,t.beginPath(),t.moveTo(f,o),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/O*m;t.lineTo(R,N),t.lineTo(R+e,N)}),t.lineTo(f+T,o),t.closePath(),t.fill(),t.globalAlpha=ht?.3:1,t.beginPath(),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/O*m;st===0?t.moveTo(R,N):t.lineTo(R,N),t.lineTo(R+e,N)}),t.stroke()}t.globalAlpha=1;const x=i=>f+(i-ot)/ut*T;t.font=`${Q(-1)}px system-ui, sans-serif`;for(const[i,w]of[[nt,"LSL"],[it,"USL"]]){if(i===void 0)continue;const A=x(i);t.strokeStyle=c.limitLine,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(A,a),t.lineTo(A,o),t.stroke(),t.setLineDash([]),t.fillStyle=c.limitLine,t.textAlign="center",t.textBaseline="bottom",t.fillText(w,A,a-2)}t.font=`${Q(-1)}px system-ui, sans-serif`,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.moveTo(f,o),t.lineTo(f+T,o),t.stroke(),t.fillStyle=c.textMuted,t.textAlign="center",t.textBaseline="top";const lt=Math.max(2,Math.floor(T/44)),E=Math.max(1,Math.ceil(u.length/lt));for(let i=0;i<=u.length;i+=E){const w=i<u.length?u[i].rangeLow:u[u.length-1].rangeHigh,A=f+i*e;t.beginPath(),t.moveTo(A,o),t.lineTo(A,o+4),t.stroke(),t.fillText(U.tick(w),A,o+6)}U.unitLabel&&(t.save(),t.font=`${Q(-1)}px system-ui, sans-serif`,t.fillStyle=c.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${U.unitLabel})`,f+T,o+20),t.restore())}function F(n){const{plotX:g,plotMaxWidth:L,barWidth:t}=Y;if(n<g||n>g+L)return-1;const c=Math.floor((n-g)/t);return c>=0&&c<u.length?c:-1}k.addEventListener("mousemove",n=>{const g=k.getBoundingClientRect(),L=F(n.clientX-g.left);if(L!==D&&(D=L,M()),L>=0){const t=u[L],c=$.map((f,T)=>`<span style="${ie(_(T))};margin-right:4px"></span>${f.groupKey}: ${f.counts[L]}`).join("<br>");C.innerHTML=`<strong>${bt(t.rangeLow,v,"engineering")} \u2013 ${bt(t.rangeHigh,v,"engineering")}</strong><br>${c}`,C.style.display="block",Wt(C,r,n.clientX,n.clientY)}else C.style.display="none"}),k.addEventListener("mouseleave",()=>{D!==-1&&(D=-1,M()),C.style.display="none"}),dt=Pt(r,()=>M()),M()}j();function Gt(s){V.set(s)&&(d=s,j())}function Vt(s){gt.set(s)&&j()}return{card:r,setTest:Gt,setAxisPrefs:Vt,destroy:()=>dt?.disconnect()}}
|
|
1
|
+
import{buildTestHistogramData as _t,collectTestValues as At,buildTestHistogramSeries as Ut,testValueExtent as Ct}from"../../stats/histogram.js";import{SPACE as at,fontPx as Q,FONT as $t,CLR as Pt}from"../toolbar.js";import{fmt as bt}from"../../renderer/fmt.js";import{QUANTITY as Xt,categorical as Yt}from"./palette.js";import{cardShell as Qt,observeResize as Wt,makeTooltip as qt,attachChartTip as Jt,positionChartTooltip as Dt,makeLinkedTestSelect as Zt,makeWaferSelect as te,makeLinkedAxisPrefs as ee,renderEmptyState as Tt,chartFillHeight as Et,applyCanvasFlow as It,makeAxisFormat as Bt,PADDING as ft,robustFence as Ft,shouldIncludeLimitsByDefault as Kt,drawOffAxisLimits as ne,resolveAxisRange as ie,chartSwatchCss as oe,makeSeriesLegendItem as le,prepareCanvas as Ot}from"./chartShell.js";import{escHtml as St}from"../../core/utils.js";const wt=230,Rt=36,yt=18;function Nt(l,G,q,J,Mt,B,r,y=4){const Z=Math.max(1,B/y),rt=Math.pow(10,Math.floor(Math.log10(Z))),d=Z/rt,tt=(d<=1?1:d<=2?2:d<=5?5:10)*rt,K=J-q;l.save(),l.font=`${Q(-1)}px system-ui, sans-serif`,l.textAlign="right",l.textBaseline="middle";for(let V=0;V<=B+1e-9;V+=tt){const ct=J-V/(B||1)*K;l.strokeStyle=r.grid,l.lineWidth=1,l.beginPath(),l.moveTo(G,ct),l.lineTo(G+Mt,ct),l.stroke(),l.fillStyle=r.text,l.fillText(String(Math.round(V)),G-4,ct)}l.strokeStyle=r.axis,l.lineWidth=1,l.beginPath(),l.moveTo(G,q),l.lineTo(G,J),l.stroke(),l.restore()}export function renderHistogramPanel(l){const{title:G="Test value distribution",items:q,testDefs:J,onSaveImage:Mt,groups:B}=l,{card:r,body:y,controlsRow:Z}=Qt(G,Mt,l.ownerDocument),rt=J.filter(s=>s.testNumber!==void 0);let d=l.selectedTestNumber??rt[0]?.testNumber??null,tt=null,K=0;const V=Zt(rt,d,s=>{d=s,j(),l.onTestChange?.(s)},{maxWidth:"200px",ownerDocument:r.ownerDocument}),ct=V.el;Z.appendChild(ct);const vt=te(q,tt,s=>{tt=s,j()},{ownerDocument:r.ownerDocument});Z.appendChild(vt);function zt(s){return S===s||s!==null&&!(B??[]).some(v=>v.key===s)?!1:(S=s,j(),!0)}const Lt=r.ownerDocument.createElement("span");Object.assign(Lt.style,{display:"inline-flex",gap:at.lg,alignItems:"center"}),Z.appendChild(Lt);const gt=ee(Lt,l.axisPrefs,s=>{l.onAxisPrefsChange?.(s),j()},r.ownerDocument),C=qt(r);let dt=null,S=null;function et(s){const v=J.find(nt=>nt.testNumber===s);return{unit:v?.unit,limitLow:v?.limitLow,limitHigh:v?.limitHigh}}function j(){const{includeLimits:s,clipOutliers:v}=gt.get();if(dt?.disconnect(),dt=null,y.innerHTML="",d===null){Tt(y,"No parametric test data available for a histogram.");return}const nt=!!B&&B.length>0;let it=!1,u;if(nt){const{limitLow:a,limitHigh:m}=et(d),o=B.flatMap(x=>x.items),{min:e,max:p}=Ct(o,d);if(it=s??Kt(e,p,a,m),v){const x=At(o,d),H=Ft(x);H&&(u={lo:Math.max(e,H.lo),hi:Math.min(p,H.hi)},K=x.reduce((lt,E)=>lt+(E<u.lo||E>u.hi?1:0),0))}u||(K=0)}const $=nt?Ut(B,d,16,it?et(d).limitLow:void 0,it?et(d).limitHigh:void 0,u):null;if(vt.style.display=$?"none":"",$){const{limitLow:a,limitHigh:m}=et(d);if(gt.sync(it,a!==void 0||m!==void 0),$.series.length===0){Tt(y,"No parametric test data available for a histogram.");return}Gt($);return}const _=tt!==null?[q[tt]]:q,{unit:O,limitLow:P,limitHigh:W}=et(d),k=At(_,d),{min:ot,max:mt}=Ct(_,d),ut=s??Kt(ot,mt,P,W);gt.sync(ut,P!==void 0||W!==void 0);const U=v?Ft(k):null,X=U?{lo:Math.max(ot,U.lo),hi:Math.min(mt,U.hi)}:void 0;K=X?k.filter(a=>a<X.lo||a>X.hi).length:0;const h=_t(_,d,16,ut?P:void 0,ut?W:void 0,X);if(h.length===0){Tt(y,"No parametric test data available for a histogram.");return}const D=Math.max(...h.map(a=>a.count),1),Y=r.ownerDocument.createElement("div");Object.assign(Y.style,{fontSize:$t.body,color:Pt.label,marginBottom:at.xxs}),Y.textContent=`max ${D} dies/bucket`+(K?` \xB7 ${K} value${K===1?"":"s"} outside clipped range`:""),y.appendChild(Y);const M=r.ownerDocument.createElement("canvas");M.style.display="block",M.style.cursor="default",y.appendChild(M);let F=-1;const n=h[0].rangeLow,g=h[h.length-1].rangeHigh,L=g-n||1,t=Bt(Math.max(Math.abs(n),Math.abs(g)),O);function c(a){const m=ft+36,o=M.clientWidth-m-ft,e=a-Rt-yt;return{plotX:m,plotMaxWidth:Math.max(10,o),plotMaxHeight:e,plotTop:yt}}function f(a){const{plotX:m,plotMaxWidth:o}=c(wt);if(a<m||a>m+o)return-1;const e=Math.floor((a-m)/o*h.length);return e>=0&&e<h.length?e:-1}function T(){It(M,Y);const a=y.clientWidth,m=Et(r,y,M,wt),o=Ot(M,r,a,m);if(!o)return;const{ctx:e,theme:p}=o,{plotX:x,plotMaxWidth:H,plotMaxHeight:lt,plotTop:E}=c(m),i=E+lt,w=H/h.length;Nt(e,x,E,i,H,D,{text:p.textMuted,axis:p.border,grid:p.border}),h.forEach((b,z)=>{const I=b.count/D*lt,kt=x+z*w,Ht=i-I;e.fillStyle=z===F?p.text:Xt,e.globalAlpha=z===F?1:.7,e.fillRect(kt+1,Ht,Math.max(1,w-2),I),e.globalAlpha=1,z===F&&(e.fillStyle=p.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${b.count}`,kt+w/2,Ht-2))});const A=b=>x+(b-n)/L*H;e.font=`${Q(-1)}px system-ui, sans-serif`;const{offAxis:ht}=ie({dataMin:n,dataMax:g,limitLow:P,limitHigh:W,includeLimits:!1});ne(e,ht,{left:x,right:x+H,top:E,bottom:i},"horizontal",p.limitLine,t.tick);const pt=new Set(ht.map(b=>b.value));for(const[b,z]of[[P,"LSL"],[W,"USL"]]){if(b===void 0||pt.has(b))continue;const I=A(b);e.strokeStyle=p.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(I,E),e.lineTo(I,i),e.stroke(),e.setLineDash([]),e.fillStyle=p.limitLine,e.textAlign="center",e.textBaseline="bottom",e.fillText(z,I,E-2)}e.font=`${Q(-1)}px system-ui, sans-serif`,e.strokeStyle=p.border,e.lineWidth=1,e.beginPath(),e.moveTo(x,i),e.lineTo(x+H,i),e.stroke(),e.fillStyle=p.textMuted,e.textAlign="center",e.textBaseline="top";const st=Math.max(2,Math.floor(H/44)),R=Math.ceil(h.length/st),N=Math.max(1,R);for(let b=0;b<=h.length;b+=N){const z=b<h.length?h[b].rangeLow:h[h.length-1].rangeHigh,I=x+b*w;e.beginPath(),e.moveTo(I,i),e.lineTo(I,i+4),e.stroke(),e.fillText(t.tick(z),I,i+6)}t.unitLabel&&(e.save(),e.font=`${Q(-1)}px system-ui, sans-serif`,e.fillStyle=p.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${t.unitLabel})`,x+H,i+20),e.restore())}M.addEventListener("mousemove",a=>{const m=M.getBoundingClientRect(),o=f(a.clientX-m.left);if(o!==F&&(F=o,T()),o>=0){const e=h[o];C.innerHTML=`<strong>${St(`${bt(e.rangeLow,O,"engineering")} \u2013 ${bt(e.rangeHigh,O,"engineering")}`)}</strong><br>${e.count} dies`,C.style.display="block",Dt(C,r,a.clientX,a.clientY)}else C.style.display="none"}),M.addEventListener("mouseleave",()=>{F!==-1&&(F=-1,T()),C.style.display="none"}),dt=Wt(r,()=>T()),T()}function Gt(s){const{unit:v,limitLow:nt,limitHigh:it}=et(d),u=s.ranges,$=s.series,_=Yt;S&&!$.some(n=>n.groupKey===S)&&(S=null);const O=Math.max(1,...$.flatMap(n=>n.counts)),P=r.ownerDocument.createElement("div");Object.assign(P.style,{fontSize:$t.body,color:Pt.label,marginBottom:at.xxs}),P.textContent=`${$.length} groups \xB7 max ${O} dies/bucket`,y.appendChild(P);const W=r.ownerDocument.createElement("div");Object.assign(W.style,{display:"flex",flexWrap:"wrap",gap:at.sm,marginTop:at.md,marginBottom:at.xl}),$.forEach((n,g)=>{const L=le(n.groupKey,_(g),r.ownerDocument),t=L.el;Jt(t,r,C,`${n.groupKey} \u2014 click to emphasize (dim the rest)`),L.setState({selected:S===n.groupKey,dimmed:S!==null&&S!==n.groupKey}),t.addEventListener("click",()=>{const c=S===n.groupKey?null:n.groupKey;zt(c),l.onGroupChange?.(c)}),W.appendChild(t)}),y.appendChild(W);const k=r.ownerDocument.createElement("canvas");k.style.display="block",k.style.cursor="default",y.appendChild(k);const ot=u[0].rangeLow,mt=u[u.length-1].rangeHigh,ut=mt-ot||1,U=Bt(Math.max(Math.abs(ot),Math.abs(mt)),v),X=n=>{const g=(n.ownerDocument.defaultView??window).getComputedStyle(n);return n.offsetHeight+(parseFloat(g.marginTop)||0)+(parseFloat(g.marginBottom)||0)},h=()=>X(P)+X(W);let D=-1,Y={plotX:ft+36,plotMaxWidth:10,barWidth:10};function M(){It(k,h());const n=y.clientWidth,g=Et(r,y,k,wt),L=Ot(k,r,n,g);if(!L)return;const{ctx:t,theme:c}=L,f=ft+36,T=Math.max(10,n-f-ft),a=yt,m=g-Rt-yt,o=a+m,e=T/u.length;Y={plotX:f,plotMaxWidth:T,barWidth:e},Nt(t,f,a,o,T,O,{text:c.textMuted,axis:c.border,grid:c.border}),D>=0&&(t.fillStyle=c.bgHover,t.globalAlpha=.5,t.fillRect(f+D*e,a,e,m),t.globalAlpha=1);const p=$.map((i,w)=>({s:i,i:w})).sort((i,w)=>(i.s.groupKey===S?1:0)-(w.s.groupKey===S?1:0));for(const{s:i,i:w}of p){const A=S===i.groupKey,ht=S!==null&&!A,pt=_(w);t.strokeStyle=pt,t.fillStyle=pt,t.lineWidth=A?2:1,t.globalAlpha=ht?.12:S?.32:.22,t.beginPath(),t.moveTo(f,o),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/O*m;t.lineTo(R,N),t.lineTo(R+e,N)}),t.lineTo(f+T,o),t.closePath(),t.fill(),t.globalAlpha=ht?.3:1,t.beginPath(),i.counts.forEach((xt,st)=>{const R=f+st*e,N=o-xt/O*m;st===0?t.moveTo(R,N):t.lineTo(R,N),t.lineTo(R+e,N)}),t.stroke()}t.globalAlpha=1;const x=i=>f+(i-ot)/ut*T;t.font=`${Q(-1)}px system-ui, sans-serif`;for(const[i,w]of[[nt,"LSL"],[it,"USL"]]){if(i===void 0)continue;const A=x(i);t.strokeStyle=c.limitLine,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(A,a),t.lineTo(A,o),t.stroke(),t.setLineDash([]),t.fillStyle=c.limitLine,t.textAlign="center",t.textBaseline="bottom",t.fillText(w,A,a-2)}t.font=`${Q(-1)}px system-ui, sans-serif`,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.moveTo(f,o),t.lineTo(f+T,o),t.stroke(),t.fillStyle=c.textMuted,t.textAlign="center",t.textBaseline="top";const lt=Math.max(2,Math.floor(T/44)),E=Math.max(1,Math.ceil(u.length/lt));for(let i=0;i<=u.length;i+=E){const w=i<u.length?u[i].rangeLow:u[u.length-1].rangeHigh,A=f+i*e;t.beginPath(),t.moveTo(A,o),t.lineTo(A,o+4),t.stroke(),t.fillText(U.tick(w),A,o+6)}U.unitLabel&&(t.save(),t.font=`${Q(-1)}px system-ui, sans-serif`,t.fillStyle=c.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${U.unitLabel})`,f+T,o+20),t.restore())}function F(n){const{plotX:g,plotMaxWidth:L,barWidth:t}=Y;if(n<g||n>g+L)return-1;const c=Math.floor((n-g)/t);return c>=0&&c<u.length?c:-1}k.addEventListener("mousemove",n=>{const g=k.getBoundingClientRect(),L=F(n.clientX-g.left);if(L!==D&&(D=L,M()),L>=0){const t=u[L],c=$.map((f,T)=>`<span style="${oe(_(T))};margin-right:4px"></span>${St(`${f.groupKey}: ${f.counts[L]}`)}`).join("<br>");C.innerHTML=`<strong>${St(`${bt(t.rangeLow,v,"engineering")} \u2013 ${bt(t.rangeHigh,v,"engineering")}`)}</strong><br>${c}`,C.style.display="block",Dt(C,r,n.clientX,n.clientY)}else C.style.display="none"}),k.addEventListener("mouseleave",()=>{D!==-1&&(D=-1,M()),C.style.display="none"}),dt=Wt(r,()=>M()),M()}j();function Vt(s){V.set(s)&&(d=s,j())}function jt(s){gt.set(s)&&j()}return{card:r,setTest:Vt,setAxisPrefs:jt,destroy:()=>dt?.disconnect()}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildTestPassRateData as
|
|
1
|
+
import{buildTestPassRateData as F,hasJudgeableTests as B}from"../../stats/testPassRate.js";import{categorical as I,QUANTITY as N}from"./palette.js";import{SPACE as H,FONT as _,CLR as w}from"../toolbar.js";import{cardShell as A,makeTooltip as O,makeSegmented as j,renderEmptyState as y}from"./chartShell.js";import{renderGroupedBarPlot as M}from"./groupedBarPlot.js";import{escHtml as g}from"../../core/utils.js";const k=100,G=8,U={spec:"Spec limits",testFlag:"Tester flag",functional:"Functional"},K={spec:"Parametric pass rate \xB7 spec limits",testFlag:"Parametric pass rate \xB7 tester flag",functional:"Functional test pass rate"};export function renderTestPassRatePanel(f){const{groups:u,testDefs:b,onSaveImage:$}=f,p=f.ownerDocument??document,P=u.flatMap(e=>e.items.flatMap(a=>a.dies??[])),d=["spec","testFlag","functional"].filter(e=>B(b,e,P));let s=d[0]??"spec";const h=e=>K[e],{card:o,heading:v,body:i,controlsRow:x}=A(h(s),$,p);o.style.alignSelf="start",d.length>1&&x.appendChild(j(d.map(e=>[e,U[e]]),s,e=>{s=e,v.textContent=h(s),L()},p));const n=p.createElement("div");Object.assign(n.style,{color:w.label,fontSize:_.body,marginBottom:H.sm}),o.insertBefore(n,i);const D=O(o);let m=null;function L(){if(m?.destroy(),m=null,i.innerHTML="",d.length===0){n.textContent="",y(i,"No tests with a pass/fail verdict \u2014 parametric tests need spec limits, functional tests need recorded results.");return}const e=F(u,b,s);if(e.rows.length===0){n.textContent="",y(i,s==="spec"?"No parametric test has spec limits to judge against.":s==="testFlag"?"No parametric test has a recorded tester verdict.":"No functional test has recorded results.");return}const a=e.groups.length>1,R=s==="spec"?"Dies within spec limits, per test":s==="testFlag"?"Dies the tester marked pass, per test":"Dies passing the recorded verdict, per test";if(n.textContent=R+(a?" \xB7 one bar per group":"")+" \xB7 worst first",e.disagreementDies){const t=p.createElement("span");t.textContent=` \xB7 ${e.disagreementDies.toLocaleString()} die${e.disagreementDies===1?"":"s"} judged differently by limits and tester flag`,Object.assign(t.style,{color:w.warnText}),n.appendChild(t)}const S=e.rows,C=t=>a?I(t):N;m=M(o,i,D,{rows:S.map(t=>({label:t.label,bars:t.byGroup.map(l=>({fraction:l.passRatePercent===null?null:l.passRatePercent/100})),trailing:t.overall.passRatePercent===null?"\u2014":`${t.overall.passRatePercent.toFixed(1)}%`})),groups:e.groups,labelWidth:k,maxVisibleRows:G,showLegend:a,maxLabelChars:14,emptyBarText:"no data",colorOf:C,tooltipHtml:(t,l)=>{const r=S[t],c=r.byGroup[l],T=a?` \xB7 ${g(String(e.groups[l]))}`:"";if(c.passRatePercent===null)return`<strong>${g(r.label)}</strong>${T}<br>no dies with a verdict`;const E=s==="spec"&&!a&&(r.failLowDies||r.failHighDies)?`<br>${r.failLowDies??0} below LSL \xB7 ${r.failHighDies??0} above USL`:"";return`<strong>${g(r.label)}</strong>${T}<br>${c.passRatePercent.toFixed(1)}% pass<br>${c.passDies.toLocaleString()} pass \xB7 ${c.failDies.toLocaleString()} fail \xB7 n = ${c.totalDies.toLocaleString()}`+E}})}return L(),{card:o,destroy:()=>{m?.destroy(),D.remove(),o.remove()}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildTestTrendData as
|
|
1
|
+
import{buildTestTrendData as fe,trendCentre as ue}from"../../stats/trend.js";import{isParametricTest as he}from"../../renderer/buildWaferMap.js";import{fmt as D}from"../../renderer/fmt.js";import{SPACE as oe,fontPx as pe,FONT as ge,CLR as xe}from"../toolbar.js";import{QUANTITY as _}from"./palette.js";import{cardShell as be,observeResize as ye,makeTooltip as ve,positionChartTooltip as Te,makeLinkedTestSelect as Le,makeLinkedAxisPrefs as we,renderEmptyState as z,growCardToFitContent as ke,chartFillHeight as Ce,PADDING as X,resolveAxisRange as Se,shouldIncludeLimitsByDefault as Ae,drawOffAxisLimits as Pe,prepareCanvas as Ne}from"./chartShell.js";import{escHtml as j}from"../../core/utils.js";const le=220,De=56,He=34,Me=3.5;export function renderTrendPanel(g){const{items:G,testDefs:se,onSaveImage:re,onOpen:C,centreLabel:U="mean of all wafers"}=g,ae=g.title??"Wafer-to-wafer trend",{card:l,body:f,controlsRow:Y}=be(ae,re,g.ownerDocument);l.style.alignSelf="start";const H=se.filter(o=>o.testNumber!==void 0&&he(o));let h=g.selectedTestNumber??H[0]?.testNumber??null,I=0;const Q=Le(H,h,o=>{h=o,S(),g.onTestChange?.(o)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:l.ownerDocument});Y.appendChild(Q.el);const O=l.ownerDocument.createElement("span");Object.assign(O.style,{display:"inline-flex",gap:oe.lg,alignItems:"center"}),Y.appendChild(O);const $=we(O,g.axisPrefs,o=>{g.onAxisPrefsChange?.(o),S()},l.ownerDocument),T=l.ownerDocument.createElement("div");Object.assign(T.style,{color:xe.label,fontSize:ge.body,marginBottom:oe.sm}),l.insertBefore(T,f);const L=ve(l);let M=null,n=[];function ce(o){return H.find(B=>B.testNumber===o)}function S(){const{includeLimits:o,clipOutliers:B}=$.get();if(f.innerHTML="",M?.disconnect(),M=null,H.length===0||h===null){T.textContent="",z(f,"No parametric test data available for a trend.");return}if(G.length<2){T.textContent="",z(f,"A wafer-to-wafer trend needs at least two wafers.");return}if(n=fe(G,h),!n.some(r=>r.count>0)){T.textContent="",z(f,"No values recorded for this test.");return}const s=ce(h),x=ue(n),q=()=>{T.textContent=`Point = wafer mean, whisker = \xB11\u03C3, dashed = ${U} \xB7 wafers in slot order`+(C?" \xB7 click a point to open that wafer":"")+(I?` \xB7 axis clipped, ${I} outside`:"")};q();const u=l.ownerDocument.createElement("canvas");u.style.display="block",f.appendChild(u);let A=null;const J=()=>{const r=Math.max(1,f.clientWidth),d=Math.max(le,Ce(l,f,u,le)),i=Ne(u,l,r,d);if(!i)return;const{ctx:e,theme:b}=i;let P=1/0,N=-1/0;const V=[];for(const t of n)!t.count||!Number.isFinite(t.mean)||(P=Math.min(P,t.mean-t.stddev),N=Math.max(N,t.mean+t.stddev),V.push(t.mean-t.stddev,t.mean+t.stddev));if(!Number.isFinite(P)||!Number.isFinite(N))return;const Z=o??Ae(P,N,s?.limitLow,s?.limitHigh);$.sync(Z,s?.limitLow!==void 0||s?.limitHigh!==void 0);const E=Se({dataMin:P,dataMax:N,limitLow:s?.limitLow,limitHigh:s?.limitHigh,includeLimits:Z,clipOutliers:B,values:V});I=E.clippedCount,q();let p=E.lo,w=E.hi;const ee=(w-p)*.08;p-=ee,w+=ee;const a=X+De;e.font=`${pe(-1)}px system-ui, sans-serif`;const te=t=>t.length>8?`${t.slice(0,7)}\u2026`:t,me=n.length?Math.ceil(Math.max(e.measureText(te(n[0].label)).width,e.measureText(te(n[n.length-1].label)).width)/2):0,y=r-X-me,F=X,k=d-He,v=t=>k-(t-p)/(w-p)*(k-F),ne=n.length>1?(y-a)/(n.length-1):0;A={left:a,step:ne};const R=t=>n.length>1?a+t*ne:(a+y)/2;e.strokeStyle=b.border,e.lineWidth=1,e.beginPath(),e.moveTo(a,F),e.lineTo(a,k),e.lineTo(y,k),e.stroke(),e.fillStyle=b.text,e.textAlign="right",e.textBaseline="middle";for(const t of[0,.5,1]){const c=p+(w-p)*t;e.fillText(D(c,s?.unit),a-6,v(c))}Pe(e,E.offAxis,{left:a,right:y,top:F,bottom:k},"vertical",b.limitLine,t=>D(t,s?.unit));for(const[t,c]of[[s?.limitLow,"LSL"],[s?.limitHigh,"USL"]]){if(t===void 0||t<p||t>w)continue;const m=v(t);e.save(),e.strokeStyle=b.limitLine,e.globalAlpha=.7,e.setLineDash([4,3]),e.beginPath(),e.moveTo(a,m),e.lineTo(y,m),e.stroke(),e.restore(),e.save(),e.fillStyle=b.limitLine,e.textAlign="left",e.fillText(c,a+3,m-6),e.restore()}if(x!==null&&x>=p&&x<=w){const t=v(x);e.save(),e.strokeStyle=b.text,e.globalAlpha=.5,e.setLineDash([3,3]),e.beginPath(),e.moveTo(a,t),e.lineTo(y,t),e.stroke(),e.restore()}e.strokeStyle=_,e.lineWidth=1.5,e.beginPath();let W=!1;n.forEach((t,c)=>{if(!t.count||!Number.isFinite(t.mean)){W=!1;return}const m=R(c),ie=v(t.mean);W?e.lineTo(m,ie):e.moveTo(m,ie),W=!0}),e.stroke(),n.forEach((t,c)=>{if(!t.count||!Number.isFinite(t.mean))return;const m=R(c);t.stddev>0&&(e.save(),e.strokeStyle=_,e.globalAlpha=.55,e.lineWidth=1,e.beginPath(),e.moveTo(m,v(t.mean-t.stddev)),e.lineTo(m,v(t.mean+t.stddev)),e.stroke(),e.restore()),e.fillStyle=_,e.beginPath(),e.arc(m,v(t.mean),Me,0,Math.PI*2),e.fill()}),e.fillStyle=b.text,e.textBaseline="top",e.textAlign="center";const de=Math.max(1,Math.ceil(n.length*42/Math.max(1,y-a)));n.forEach((t,c)=>{if(c%de!==0&&c!==n.length-1)return;const m=t.label.length>8?`${t.label.slice(0,7)}\u2026`:t.label;e.fillText(m,R(c),k+6)}),ke(l,f,d)},K=r=>{if(!A||A.step<=0)return-1;const d=u.getBoundingClientRect(),i=Math.round((r.clientX-d.left-A.left)/A.step);return i>=0&&i<n.length&&n[i].count>0?i:-1};u.addEventListener("mousemove",r=>{const d=K(r);if(d<0){L.style.display="none",u.style.cursor="default";return}const i=n[d];u.style.cursor=C&&i.key!==void 0?"pointer":"default",L.innerHTML=`<strong>${j(i.label)}</strong><br>`+j(`mean ${D(i.mean,s?.unit)} \xB7 \u03C3 ${D(i.stddev,s?.unit)}`)+`<br>n = ${i.count.toLocaleString()}`+(x!==null?"<br>"+j(`\u0394 vs ${U} ${i.mean-x>=0?"+":""}${D(i.mean-x,s?.unit)}`):"")+(C&&i.key!==void 0?"<br><em>click to open this wafer</em>":""),L.style.display="block",Te(L,l,r.clientX,r.clientY)}),u.addEventListener("mouseleave",()=>{L.style.display="none"}),u.addEventListener("click",r=>{const d=K(r);if(d<0||!C)return;const i=n[d];i.key!==void 0&&h!==null&&C(i.key,h)}),J(),M=ye(f,J)}return S(),{card:l,destroy:()=>{M?.disconnect(),L.remove(),l.remove()},setTest:o=>{Q.set(o)&&(h=o,S())},setAxisPrefs:o=>{$.set(o)&&S()}}}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { toCanvas as toCanvasImpl } from './toCanvas.js';
|
|
2
2
|
import { buildDieListSection as buildDieListSectionImpl } from './dieList.js';
|
|
3
|
-
import { openReportModal as openReportModalImpl } from './toolbar.js';
|
|
4
3
|
/** @deprecated Removed in 0.31.0. It belongs to the low-level drawing pipeline, which is being withdrawn: build with `buildWaferMap` and draw with `renderWaferMap` or `renderWaferGallery`. */
|
|
5
4
|
export declare const toCanvas: typeof toCanvasImpl;
|
|
6
5
|
/** @deprecated Removed in 0.31.0. `renderWaferMap` and `renderWaferGallery` show the die list themselves (the `dieList` option). */
|
|
7
6
|
export declare const buildDieListSection: typeof buildDieListSectionImpl;
|
|
8
|
-
/** @deprecated Removed in 0.31.0. Reports open from the Summary panel's report button; route them into your host with `setReportOpener`. */
|
|
9
|
-
export declare const openReportModal: typeof openReportModalImpl;
|
|
10
7
|
//# sourceMappingURL=deprecated.d.ts.map
|