@wafertools/wafermap 0.27.0 → 0.29.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 +10 -2
- package/CHANGELOG.md +366 -0
- package/README.md +1 -1
- package/dist/packages/canvas-adapter/charts/binCluster.d.ts +0 -1
- package/dist/packages/canvas-adapter/charts/boxplot.d.ts +8 -1
- package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
- package/dist/packages/canvas-adapter/charts/capability.d.ts +0 -1
- package/dist/packages/canvas-adapter/charts/correlation.d.ts +0 -1
- package/dist/packages/canvas-adapter/charts/histogram.d.ts +0 -1
- package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +0 -1
- package/dist/packages/canvas-adapter/charts/scatter.d.ts +6 -1
- package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
- package/dist/packages/canvas-adapter/charts/trend.d.ts +7 -0
- package/dist/packages/canvas-adapter/charts/trend.js +1 -1
- package/dist/packages/canvas-adapter/hitGrid.d.ts +22 -0
- package/dist/packages/canvas-adapter/hitGrid.js +1 -0
- package/dist/packages/canvas-adapter/index.d.ts +1 -0
- package/dist/packages/canvas-adapter/index.js +1 -1
- package/dist/packages/canvas-adapter/insightsTab.d.ts +24 -2
- package/dist/packages/canvas-adapter/insightsTab.js +1 -1
- package/dist/packages/canvas-adapter/maplessSummary.d.ts +8 -1
- package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +45 -1
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +15 -5
- 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 +46 -11
- 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 +27 -16
- 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.d.ts +25 -0
- package/dist/packages/canvas-adapter/warnings.js +1 -1
- package/dist/packages/core/dies.d.ts +11 -0
- package/dist/packages/core/dies.js +1 -1
- package/dist/packages/renderer/binColors.d.ts +80 -0
- package/dist/packages/renderer/binColors.js +1 -0
- package/dist/packages/renderer/buildView.d.ts +82 -6
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.d.ts +8 -2
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/renderer/colorMap.d.ts +16 -15
- package/dist/packages/renderer/colorMap.js +1 -1
- package/dist/packages/renderer/colorSchemes.d.ts +57 -25
- package/dist/packages/renderer/colorSchemes.js +1 -1
- package/dist/packages/renderer/index.d.ts +3 -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/histogram.d.ts +28 -1
- package/dist/packages/stats/histogram.js +1 -1
- package/dist/packages/stats/population.d.ts +23 -0
- package/dist/packages/stats/population.js +1 -0
- package/dist/packages/stats/renderFindingsReport.js +10 -10
- package/dist/packages/stats/renderSummaryReport.js +22 -22
- package/dist/packages/stats/yield.js +1 -1
- package/package.json +5 -3
package/AGENTS.md
CHANGED
|
@@ -19,7 +19,7 @@ loudly over guessing.
|
|
|
19
19
|
|
|
20
20
|
### Entry points
|
|
21
21
|
|
|
22
|
-
- `@wafertools/wafermap` — `buildWaferMap()`, geometry, `
|
|
22
|
+
- `@wafertools/wafermap` — `buildWaferMap()`, geometry, `registerBinColorScheme()` / `registerValueColorScheme()`. Pure, no DOM, server-safe.
|
|
23
23
|
- `@wafertools/wafermap/render` — `renderWaferMap()`, `renderWaferGallery()`, `toCanvas()`. Needs the DOM.
|
|
24
24
|
- `@wafertools/wafermap/stats` — `analyzeWaferMap()`, `analyzeWaferLot()`. Pure analysis.
|
|
25
25
|
- `@wafertools/wafermap/worker` — `createWafermapWorker()` for off-main-thread builds.
|
|
@@ -82,6 +82,12 @@ correctness guarantee that comes with it.
|
|
|
82
82
|
- `retestPolicy: 'best'`/`'worst'` is pass/fail-aware via `passBins`; bin number only
|
|
83
83
|
breaks ties within a category.
|
|
84
84
|
- Hard bins (`hbin`) and soft bins (`sbin`) are independent number spaces. Never merge them.
|
|
85
|
+
- **Never pick a bin's colour from its number.** Bin colours are assigned by
|
|
86
|
+
`resolveBinColors` — pass bins (per `passBins`) take green pass colours, fail bins
|
|
87
|
+
take the rest, most populous first — and a rendered map exposes the result as
|
|
88
|
+
`View.binColors`. A legend or chart you build yourself must read those, or it
|
|
89
|
+
will name colours the map is not drawing. Bin maps and value maps have separate
|
|
90
|
+
schemes: `binColorScheme` and `valueColorScheme`.
|
|
85
91
|
- Build once, render many: `buildWaferMap()` handles data + geometry; re-render UI
|
|
86
92
|
changes through the controller's `setOptions()`, not by rebuilding.
|
|
87
93
|
- `result.view` is internal. Use the promoted fields: `result.plotMode`,
|
|
@@ -151,7 +157,9 @@ it is handed, because it has no way to know which tests anyone will look at.
|
|
|
151
157
|
| `WaferFlat`, field `flat` | `WaferNotch`, field `notch` |
|
|
152
158
|
| `isInsideWaferWithFlat` | `isInsideWafer` |
|
|
153
159
|
| `DieSample` / `WaferMapPoint` | `DieResult` |
|
|
154
|
-
| `colorScheme
|
|
160
|
+
| `colorScheme` / `WaferViewOptions.colorScheme` | `binColorScheme` (bin maps) and `valueColorScheme` (value and stacked maps) |
|
|
161
|
+
| `registerColorScheme` / `getColorScheme` / `listColorSchemes` | `registerBinColorScheme` / `registerValueColorScheme` and their `get` / `list` pairs |
|
|
162
|
+
| `hardBinColor` / `softBinColor` / `hardBinGreyscale` | `resolveBinColors`, or `View.binColors` from a rendered map |
|
|
155
163
|
| `plotMode: 'specLimit'` | `passFailDisplay: 'spec'` |
|
|
156
164
|
| standalone `getDieAtPoint` | `hitTarget.getDieAtPoint` from `toCanvas()` |
|
|
157
165
|
| `RenderOptions.tooltipTestLimit` | (was a no-op; nothing replaces it) |
|
package/CHANGELOG.md
CHANGED
|
@@ -22,6 +22,372 @@ under `### Breaking`.
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
## [0.29.0] — 2026-09-12
|
|
26
|
+
|
|
27
|
+
### Breaking
|
|
28
|
+
|
|
29
|
+
- **The default value gradient is now Viridis, and every perceptual gradient reads low = dark,
|
|
30
|
+
high = light.** The blue–cyan–yellow–red "thermal" ramp is removed. Like every rainbow ramp its
|
|
31
|
+
lightness is not monotonic — cyan and yellow both sit near peak while blue and red are much
|
|
32
|
+
darker — so two different readings land at the same apparent intensity, and the fast hue turns
|
|
33
|
+
at cyan and yellow draw contour lines that are not in the data. On a smooth parametric map that
|
|
34
|
+
shows up as a bright ring around the wafer centre that no process step put there. Users reported
|
|
35
|
+
exactly this: being unable to judge the difference between two colours that look equally
|
|
36
|
+
"intense". `'jet'` keeps the rainbow family available for anyone who wants it, already labelled
|
|
37
|
+
as such.
|
|
38
|
+
- **`viridis`, `cividis`, `plasma`, `inferno` and `greyscale` were registered reversed, and are
|
|
39
|
+
no longer.** Each was `forValue: t => ramp(1 - t)`, putting the *bright* end of the ramp at the
|
|
40
|
+
*low* end of the data, while `default`, `traffic` and `jet` ran the other way. Switching gradient
|
|
41
|
+
therefore inverted the map. On a stacked map the effect was backwards from what the mode is for:
|
|
42
|
+
the healthy bulk of the wafer (fail count 0) rendered as a glowing yellow field and the edge
|
|
43
|
+
ring, scratch or cluster you were looking for became dark specks on it. All five now match their
|
|
44
|
+
matplotlib/seaborn definitions. `tests/colorSchemes.test.mjs` measures L\* across every
|
|
45
|
+
perceptual built-in and fails if a reversal reappears.
|
|
46
|
+
|
|
47
|
+
This also corrects the record on the change that made thermal the default in the first place
|
|
48
|
+
(0.14.x, "the reversed-Viridis ramp rendered high values dark purple and low values yellow — not
|
|
49
|
+
intuitive"). The diagnosis was right and the remedy was aimed at the wrong thing: the defect was
|
|
50
|
+
the `1 - t`, not Viridis, and the `1 - t` survived the change that was meant to fix it.
|
|
51
|
+
- **The standalone `'viridis'` gradient name is gone** — Viridis *is* `'default'`, labelled
|
|
52
|
+
**Default (Viridis)**. Two menu rows drawing identical maps is the same defect that removed the
|
|
53
|
+
old standalone "Thermal" row. `getValueColorScheme('viridis')` falls back to `'default'`, so a
|
|
54
|
+
persisted `'viridis'` preference still renders Viridis; it no longer appears in
|
|
55
|
+
`listValueColorSchemes()`.
|
|
56
|
+
- **`View.reverseValueScheme` is a new required field on `View`.** Code that builds a `View`
|
|
57
|
+
literal must supply it; code that reads one is unaffected.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **`reverseValueScheme` (`ViewOptions`, `WaferPreferences`) — flips whichever gradient is
|
|
62
|
+
selected**, offered in the Colour scheme menu as **Reverse gradient**. One flag rather than a
|
|
63
|
+
reversed twin of every ramp: it doubles no menu, and a gradient a host registered itself gets
|
|
64
|
+
the behaviour for free. It is also what makes the direction rule above exception-free —
|
|
65
|
+
greyscale ships low = dark like everything else, and the print habit of "more ink means more" is
|
|
66
|
+
one tick box rather than a documented special case.
|
|
67
|
+
- **`resolveValueColorFn(name, reversed)` (`renderer`) — the single read-path for a value
|
|
68
|
+
gradient.** Die fills, the colorbar and the mapless summary's histogram bars each looked the
|
|
69
|
+
gradient up independently; a flag applied at some of them and not others would show one reading
|
|
70
|
+
in two different colours on one screen, with the colorbar — the thing the map is read against —
|
|
71
|
+
the likeliest to be missed. All three now resolve through this. Any host colouring by value
|
|
72
|
+
should too, passing `View.valueColorScheme` and `View.reverseValueScheme` together.
|
|
73
|
+
- **`'mako'` value gradient** (seaborn), sampled at nine even stops from
|
|
74
|
+
`sns.color_palette('mako', as_cmap=True)`. The closest thing to Viridis in lightness span, with
|
|
75
|
+
more separation at the top end. Its sibling `crest` was measured alongside it and deliberately
|
|
76
|
+
not added: at L\* ~31 → ~78 against Viridis's ~10 → ~93 it has too little span for a dense die
|
|
77
|
+
grid — on a stacked map a scratch all but disappeared into the surrounding green. Seaborn pitches
|
|
78
|
+
crest at line plots, where mako's dark end gets lost; that is a different job from colouring
|
|
79
|
+
thousands of adjacent dies.
|
|
80
|
+
|
|
81
|
+
## [0.28.0] — 2026-09-11
|
|
82
|
+
|
|
83
|
+
### Breaking
|
|
84
|
+
|
|
85
|
+
- **Bin and value colours are separate preferences; `colorScheme` is removed.**
|
|
86
|
+
`WaferViewOptions.colorScheme` / `ViewOptions.colorScheme` → `binColorScheme` (Hard/Soft Bin maps)
|
|
87
|
+
and `valueColorScheme` (Test Value and the three stacked modes — a stacked-bin map is a value map,
|
|
88
|
+
each position's occurrence rate). The single option was reset to `'default'` on every switch into a
|
|
89
|
+
bin mode (the "not bin-compatible" reset, written out three times), so a value-map choice never
|
|
90
|
+
survived a mode switch and no host could persist one honestly. `View.colorScheme` →
|
|
91
|
+
`View.binColorScheme` + `View.valueColorScheme`. Both are `WaferPreferences`, reported through
|
|
92
|
+
`onViewOptionsChange` with category `'preference'`.
|
|
93
|
+
- **`registerColorScheme` / `getColorScheme` / `listColorSchemes` and `ColorScheme` are replaced by
|
|
94
|
+
two registries:** `registerBinColorScheme` / `getBinColorScheme` / `listBinColorSchemes`
|
|
95
|
+
(`BinColorScheme = { label, pass, fail }`) and `registerValueColorScheme` / `getValueColorScheme` /
|
|
96
|
+
`listValueColorSchemes` (`ValueColorScheme = { label, forValue }`). A host-registered bin palette
|
|
97
|
+
now appears in the bin-mode Palette menu — the old menu filtered bin modes by hardcoded name, so it
|
|
98
|
+
never could. `registerBinColorScheme` throws on an empty `pass` or `fail` list.
|
|
99
|
+
- **`hardBinColor`, `softBinColor`, `hardBinGreyscale` and `HARD_BIN_GREY` are removed.** They hashed
|
|
100
|
+
the bin number (see Fixed). Use `resolveBinColors`, or `View.binColors` from a rendered map.
|
|
101
|
+
- **The `'custom'` pseudo-scheme is gone.** `BinDef.color` is now a layer every bin palette honours,
|
|
102
|
+
on by default; `useDefinedBinColors: false` (Palette menu: *Use colours from bin definitions*) turns
|
|
103
|
+
it off. Definition colours used to apply only while "Custom" was selected, and the gallery switched
|
|
104
|
+
to it only when the host had not passed a scheme — so a host restoring a saved scheme silently
|
|
105
|
+
hid the colours that came with the data.
|
|
106
|
+
- **Value gradient names:** `'accessible'` → `'cividis'` (the bin half of the old scheme is now the
|
|
107
|
+
`'accessible'` bin palette, labelled *Colour-blind safe*). `'thermal'` is removed: it had the same
|
|
108
|
+
keypoints as `'default'` — two menu rows drawing identical maps.
|
|
109
|
+
- **Bin maps look different.** Both bin palettes were re-selected by measurement and colours are now
|
|
110
|
+
assigned by pass/fail and die count rather than by bin number (see Fixed), so images of bin maps
|
|
111
|
+
will not match earlier ones.
|
|
112
|
+
- Insights internals: `InsightsTabDeps.getColorSchemeName` → `getBinColors`;
|
|
113
|
+
`ScatterPanelOptions.colorScheme` → `binColors`; the unused `colorScheme` field is removed from the
|
|
114
|
+
bin-cluster, boxplot, capability, correlation, histogram and region-yield panel options.
|
|
115
|
+
|
|
116
|
+
### Added
|
|
117
|
+
|
|
118
|
+
- **`resolveBinColors(dies, options)`** — the one rule for bin colour, used by the map, its legends,
|
|
119
|
+
the summary panels, the mapless footer and the Insights charts. Pass bins (per `passBins`) take the
|
|
120
|
+
palette's pass colours, failing bins its fail colours, each ranked by die count; a soft bin passes
|
|
121
|
+
when every die carrying it passes. Returns `BinColors` (`{ hard, soft, shared }`). Exposed on every
|
|
122
|
+
view as **`View.binColors`**; **`ViewOptions.binColors`** lets a host share one assignment across
|
|
123
|
+
several maps (`renderWaferGallery` does this for every wafer it shows, so a bin is one colour on every card).
|
|
124
|
+
- **`diePassStatus(die, passBins)`** (`core`) — the per-die pass rule (hard bin, else soft bin),
|
|
125
|
+
shared by yield, the failing-die hatch and bin colouring.
|
|
126
|
+
- **`bin-colors-shared` warning** — raised by the renderers when bins on the bin map on screen share
|
|
127
|
+
a colour (more bins than the palette has colours, or a `BinDef.color` repeating one). A gallery
|
|
128
|
+
states it once for all its wafers rather than per card.
|
|
129
|
+
- `tests/binPalettes.test.mjs` re-measures the built-in bin palettes (CIEDE2000, with simulated
|
|
130
|
+
deuteranopia, protanopia and tritanopia) so an edit cannot quietly erode their separation.
|
|
131
|
+
- `docs/api.md` now documents `standardDiameters` (§4.1.12) and the metadata helpers
|
|
132
|
+
`metadataDisplayValue` / `metadataCategoricalValue` / `discoverDieMetadataKeys` (§10.1). All
|
|
133
|
+
four were public exports that the reference never mentioned — `STANDARD_WAFER_DIAMETERS_MM`
|
|
134
|
+
since 0.27.0, where it shipped as the documented escape hatch for the new
|
|
135
|
+
`non-standard-diameter` advisory and was therefore undiscoverable outside the source; the
|
|
136
|
+
other three since 0.24.0.
|
|
137
|
+
- `scripts/check-api-claims.mjs` now also fails when a public export (per
|
|
138
|
+
`tests/export-surface.test.mjs`'s snapshot, the deliberate record of what is public) is never
|
|
139
|
+
mentioned in `docs/api.md`. The bar is low on purpose — the name appearing anywhere, even in a
|
|
140
|
+
code block, is enough — because this catches exports forgotten entirely, which is the failure
|
|
141
|
+
that actually happened, not how well each one is explained.
|
|
142
|
+
- `tests/insightsScopeReconciliation.test.mjs` pins scope-aware test-definition reconciliation in
|
|
143
|
+
**all three** Insights views. The behaviour was already correct; nothing tested it, and the
|
|
144
|
+
whole-load version shipped once before.
|
|
145
|
+
- **A boxplot leaf click now does something in a single-wafer render.** `renderWaferMap`'s
|
|
146
|
+
Insights tab passes no `openWafer` — the only wafer there is to open is the one already on
|
|
147
|
+
screen — which left the "Test value distribution" row inert, silently throwing away the other
|
|
148
|
+
half of the click: the selected test. New `InsightsTabDeps.focusTest`, wired by
|
|
149
|
+
`renderWaferMap`, switches the map behind the tab into test-value mode on the clicked test
|
|
150
|
+
and closes Insights. The gallery is unchanged: where `openWafer` exists it wins, since
|
|
151
|
+
opening the wafer already lands it on that test.
|
|
152
|
+
- `BoxplotPanelOptions.openActionLabel` (optional) supplies the wording for both click
|
|
153
|
+
affordances, so the chart says what it will actually do — "click a box to show this test on
|
|
154
|
+
the map" instead of "…to open that wafer". One string, not two, because the hint and the
|
|
155
|
+
tooltip describe one action.
|
|
156
|
+
- `focusTest` is consulted only for a leaf row of the sole item: with more than one wafer on
|
|
157
|
+
screen, "show this test on the map" would show it for a different wafer than the one
|
|
158
|
+
clicked. The trend panel is deliberately not wired to it — it renders an empty state below
|
|
159
|
+
two wafers, so that branch could never fire.
|
|
160
|
+
- Reported against tsmap, where a single-wafer load has exactly this gap; logged there as
|
|
161
|
+
WMAP_ISSUES.md #51.
|
|
162
|
+
|
|
163
|
+
- `scripts/check-doc-links.mjs` (new, wired into `npm run check`) resolves every internal
|
|
164
|
+
documentation link and fails on a dead one. Zensical has no link validation and no redirect
|
|
165
|
+
map, so a wrong anchor is not an error — the browser lands at the top of the page and the
|
|
166
|
+
reader concludes the docs are wrong about themselves, with nothing in the build saying a word.
|
|
167
|
+
It checks that link targets exist, that every `#anchor` names a heading the Markdown really
|
|
168
|
+
produces (using Python-Markdown's slug algorithm, the one Zensical actually runs — validated
|
|
169
|
+
against a real build at 385/385 anchors), that no two headings in a page slugify to the same
|
|
170
|
+
id, and that every `zensical.toml` nav entry resolves. The hand-written example and demo pages
|
|
171
|
+
are scanned too, which is how `theming.html`'s `../api.html#...` was found: the site serves
|
|
172
|
+
that page at `api/`, so the link had never worked. tsmap carries the same script.
|
|
173
|
+
|
|
174
|
+
### Changed
|
|
175
|
+
|
|
176
|
+
- **"Mark failing dies" moved from Colour scheme to Overlays, and both menus gained an action
|
|
177
|
+
to undo themselves.** It was the one entry in Colour scheme that was not a scheme — a marker
|
|
178
|
+
drawn over dies, sitting among mutually exclusive palettes — while its sibling concept, the
|
|
179
|
+
Spec/Test pass-fail display, was already in Overlays. The two are the same judgement about the
|
|
180
|
+
same dies, and splitting them across two menus made neither findable from the other. Overlays
|
|
181
|
+
was already the right home in a second way: it already carries conditionally-disabled rows
|
|
182
|
+
(Reticle grid), so a bin-modes-only entry is not out of place there.
|
|
183
|
+
**Overlays gained "Clear overlays"** — six toggles are tedious to turn off one at a time, and
|
|
184
|
+
the row is greyed when nothing is on, so the menu now answers "is anything active?" without the
|
|
185
|
+
reader auditing every line.
|
|
186
|
+
**Orientation gained "Reset orientation"**, which matters more than convenience: rotation and
|
|
187
|
+
mirroring do not commute (`mirror ∘ rot(θ) = rot(−θ) ∘ mirror`), so a reader who has rotated
|
|
188
|
+
and flipped a few times cannot reliably click their way back. Reset is exactly right, and a
|
|
189
|
+
wafer map read in the wrong orientation is the class of mistake this library exists to prevent.
|
|
190
|
+
Both are greyed at the default state.
|
|
191
|
+
`CheckMenuRow` gained `action: true` for rows that *do* something rather than holding a state.
|
|
192
|
+
The first version of this change shipped them as ordinary rows, which rendered an unchecked ✓
|
|
193
|
+
and announced them to a screen reader as checkboxes that were off. The surface snapshot below
|
|
194
|
+
caught it on its first real use — the diff showed `Clear overlays [unchecked, disabled]`.
|
|
195
|
+
- **`scripts/ui-surface.mjs` (new) records the whole interactive surface to a committed text
|
|
196
|
+
file** — `docs/ui-surface.txt`, 117 controls across the single map, the gallery and the Insights
|
|
197
|
+
tab, with every label, accessible name, hint, checked/disabled state and menu ordering.
|
|
198
|
+
`npm run ui:surface` regenerates it; `npm run ui:surface:check` fails on un-recorded drift.
|
|
199
|
+
This library's chrome ships inside other people's applications, and every existing check asks
|
|
200
|
+
whether it is built correctly rather than what it says. Reading the whole surface as one page
|
|
201
|
+
is also what makes duplication and odd ordering visible — those are invisible while each menu
|
|
202
|
+
is only ever seen alone. Not in `npm run check`: it needs a browser and a built `dist/`, like
|
|
203
|
+
`screenshots`.
|
|
204
|
+
- **`WMAP_VERSION` and `WMAP_BUILD_TIME` are now public**, from `@wafertools/wafermap/render`.
|
|
205
|
+
They already existed — generated by `sync-version.mjs` on every build — but only reached an
|
|
206
|
+
internal `console.log`. A host embedding this library could tell a user which *application*
|
|
207
|
+
they were running and had no way to say which **engine** was underneath, and "check the browser
|
|
208
|
+
console" is not an answer you can give a fab engineer asking why a map looks wrong. Being
|
|
209
|
+
generated at build time, they describe the bundle actually loaded rather than whatever a nearby
|
|
210
|
+
`package.json` claims — different things whenever a host is linked to a local checkout. tsmap
|
|
211
|
+
shows both in its About dialog from 0.1.34.
|
|
212
|
+
- **The docs site gained a light/dark theme, and most of Markdown.** Zensical is bumped
|
|
213
|
+
0.0.51 → 0.0.60 (nine releases; `requirements.txt`, so CI and local builds move together),
|
|
214
|
+
and the config now uses what it offers.
|
|
215
|
+
The find that prompted it: **declaring any `[project.markdown_extensions.*]` section replaces
|
|
216
|
+
Zensical's default set wholesale rather than merging into it.** The config set only `toc` and
|
|
217
|
+
`pymdownx.highlight`, which had been silently switching off `admonition`, `abbr`, `def_list`,
|
|
218
|
+
`attr_list`, `tasklist`, `mark`, `caret` and `tilde` for the life of the site. Nothing
|
|
219
|
+
reported it, because unsupported syntax renders as ordinary paragraph text — it reads as "we
|
|
220
|
+
don't use admonitions here", not as a fault. Verified by deleting the sections, at which point
|
|
221
|
+
`!!! note` and `*[ABBR]:` both started working. The full set is now listed explicitly, with
|
|
222
|
+
that trap written down beside it. (Mermaid was never affected: Zensical renders those fences
|
|
223
|
+
natively.)
|
|
224
|
+
New: a **light/dark/system palette toggle in the header** — the site was light-only, which sat
|
|
225
|
+
oddly beside a library whose entire chrome is themeable and whose own guide had a dark-theme
|
|
226
|
+
bug fixed in 0.26.1. `docs/stylesheets/extra.css` stopped hardcoding three light greys and now
|
|
227
|
+
bridges Material's palette onto wmap's `--wmap-*` custom properties, so **the live wafer-map
|
|
228
|
+
demos follow the theme too** rather than sitting as bright panels on a dark page — the same
|
|
229
|
+
trick tsmap uses for its own 16 themes.
|
|
230
|
+
Also enabled: `toc.follow` and `navigation.top` (a 4,150-line API reference is the case they
|
|
231
|
+
exist for), `navigation.tracking` so a link copied from deep in a page points there,
|
|
232
|
+
`navigation.footer`, `search.highlight`/`search.share`, `content.code.annotate`/`select`,
|
|
233
|
+
`content.tabs.link`, and `content.tooltips` fed by a shared `includes/abbreviations.md` —
|
|
234
|
+
domain terms only (STDF, PTR, Cpk, the bin records), since tooltipping every "API", "UI" and
|
|
235
|
+
"CSV" underlined most sentences and told the reader nothing. The include lives outside `docs/`
|
|
236
|
+
because a snippet inside it is also built as a page of its own.
|
|
237
|
+
Checked rather than assumed: heading ids are byte-identical across the bump, all 670 site
|
|
238
|
+
anchors still resolve, no CLI flag was mangled by the newly-enabled `smartsymbols` (`--tests`
|
|
239
|
+
and friends survive intact), and the dark palette was driven in a real browser.
|
|
240
|
+
- **The Quick Start's synthetic-lot generator now has one home.** The same ~20 lines existed
|
|
241
|
+
four times — `docs/quickstart.md`'s copy-paste example, `docs/examples/quickstart-live.html`,
|
|
242
|
+
and a comment-stripped copy inlined in `scripts/capture-definitions.mjs` to shoot
|
|
243
|
+
`quickstart-first-map.png` — all byte-identical, with nothing enforcing it. Editing the doc
|
|
244
|
+
would have left the "open this in your browser" page and the screenshot directly beneath it
|
|
245
|
+
rendering a different wafer from the code the reader had just copied, silently.
|
|
246
|
+
`docs/examples/quickstart-data.js` is now the source: the live page and the capture import it,
|
|
247
|
+
so those cannot drift by construction. The Markdown keeps its inline copy deliberately — the
|
|
248
|
+
snippet promises "copy this into an HTML file, no bundler required", which a local import
|
|
249
|
+
would break — and `scripts/check-quickstart-snippet.mjs` (wired into `npm run check`) holds it
|
|
250
|
+
to the fixture, with `--write` to regenerate it. It also fails if either of the other two
|
|
251
|
+
re-inlines the loop. The refactor is provably inert: `quickstart-first-map.png` re-captures
|
|
252
|
+
byte-for-byte identical.
|
|
253
|
+
- **The Quick Start no longer detours into partial-wafer geometry.** The "Partial data needs a
|
|
254
|
+
wafer centre" section — three paragraphs and a second code block, arriving immediately after
|
|
255
|
+
the reader's first successful map — is now a short callout that names the case, names the
|
|
256
|
+
`'partial-coverage'` warning, and links to the guide section that already covers it in full.
|
|
257
|
+
A tutorial's job is the first success; the geometry discussion is a how-to and was already
|
|
258
|
+
written as one. Nothing was deleted, and a "you have now…" line closes the walkthrough.
|
|
259
|
+
- **The Developer Guide's sections are no longer numbered.** `## 3. Loading real data from a
|
|
260
|
+
CSV` is now `## Loading real data from a CSV`, and all 42 links that pointed at the numbered
|
|
261
|
+
anchors — plus the `Guide §N` link labels in `docs/examples/manifest.json`, which name the
|
|
262
|
+
section they lead to instead — were moved with it. The numbering was chronological by feature
|
|
263
|
+
addition rather than by reading order, and it was baked into every anchor
|
|
264
|
+
(`#3-loading-real-data-from-a-csv`), so inserting one section silently broke every link below
|
|
265
|
+
it. `check-doc-links.mjs` landed first precisely so this move could be proven complete rather
|
|
266
|
+
than assumed. The API reference keeps its numbering for now — there the numbers are a real
|
|
267
|
+
hierarchical aid, not an accident of ordering.
|
|
268
|
+
|
|
269
|
+
### Fixed
|
|
270
|
+
|
|
271
|
+
- **Implausible geometry could hang a render.** `toCanvas` sized its hit-test grid from die size
|
|
272
|
+
alone, so a die size wrong by orders of magnitude — a misread WCR record gave dies ~1e-8 mm
|
|
273
|
+
wide — asked for more cells than an array can hold: every render and resize threw
|
|
274
|
+
`Invalid array length`, and the map never appeared. The grid is now sized by `hitGridDims`
|
|
275
|
+
(internal `hitGrid.ts`), which falls back to the span for non-finite or non-positive sizes and
|
|
276
|
+
caps the grid at max(4096, 4 × dies) cells; a coarser grid only means a hit test looks at a few
|
|
277
|
+
more dies. A non-finite die position no longer indexes cell `NaN`. Found via tsmap
|
|
278
|
+
(WMAP_ISSUES.md #53).
|
|
279
|
+
- **"Lot" was used for any set of wafers, including sets spanning several lots.** A gallery or
|
|
280
|
+
`analyzeWaferLot` call covers whatever wafers were passed, but the panel read "Lot Summary —
|
|
281
|
+
26 wafers", yield outliers were "lower than the lot median", the trend chart's reference was
|
|
282
|
+
the "lot mean", reports were titled "Lot Summary" / "Lot Findings Report" and the stacked
|
|
283
|
+
tooltip gave "% of lot" — all over a pooled multi-lot load, or one with no lot ID at all. One
|
|
284
|
+
rule now names the population (`stats/population.ts`): "lot" only when every wafer records the
|
|
285
|
+
same lot ID (`Summary — Lot LOT123 · 13 wafers`, "lot median"), otherwise the wafers
|
|
286
|
+
(`Summary — 26 wafers from 2 lots`, "median of all wafers"). The stacked tooltip reads
|
|
287
|
+
"% of stacked wafers"; two gallery messages became lot-neutral. Public API names
|
|
288
|
+
(`analyzeWaferLot`, `LotStatsSummary`, `lotStack`) keep "lot" in its looser sense — the
|
|
289
|
+
glossary now says so. Also corrected: the API reference and developer guide still described
|
|
290
|
+
the gallery panel's long-gone Lot/Wafers tabs, and called its button "Lot findings" rather than
|
|
291
|
+
**Summary panel**.
|
|
292
|
+
- **Different bins were drawn in the same colour.** Soft bins and the Accessible palette hashed the
|
|
293
|
+
bin number into a fixed list, so collisions were certain: among bins 1–16, soft bins 1 and 7 and
|
|
294
|
+
4 and 6 were identical, as were Accessible bins 4 and 5 and 9 and 13, and only 75 distinct colours
|
|
295
|
+
covered hard bins 1–255. The hand-picked hard bins 1–14 came in near-duplicate pairs (1/12 green,
|
|
296
|
+
2/9 red, 5/11 blue…), and bins 4 and 11 were ΔE 1.4 apart for a deuteranope. Colours are now
|
|
297
|
+
rank-assigned from measured palettes: no two bins share a colour until the palette is exhausted,
|
|
298
|
+
and then a warning names them. Default: 3 pass + 19 fail colours, every pair ≥ 16 ΔE00. Colour-blind
|
|
299
|
+
safe: 2 pass + 14 fail colours, every pair ≥ 8.8 ΔE00 under all three simulated deficiencies.
|
|
300
|
+
- **Bin colour contradicted `passBins`.** Bin 1 was always green and bin 2 always red, whatever
|
|
301
|
+
`passBins` said — with `passBins: [1, 3]` a passing bin 3 drew orange ("marginal") and a failing
|
|
302
|
+
bin 12 drew green. Pass colours now come from `passBins`, never the number.
|
|
303
|
+
- **The soft-bin failing-die hatch marked the wrong dies.** It tested the plotted *soft* bin number
|
|
304
|
+
against the *hard* pass-bin list. It now judges each die by `diePassStatus`, the rule yield uses.
|
|
305
|
+
- **`WaferMapController.getActiveLegend()` returned colours the map was not drawing.** It hashed bin
|
|
306
|
+
numbers directly, ignoring the selected palette, so a host legend built from it disagreed with the
|
|
307
|
+
map under any non-default scheme. It now reads the view's resolved colours.
|
|
308
|
+
- **`analyzeWaferMap` reported a "correction" for an option that was never set.**
|
|
309
|
+
`{ ...DEFAULT_OPTIONS, ...options }` let an explicit `undefined` overwrite the default, which
|
|
310
|
+
then failed the finite-number test — so the most ordinary thing a host writes (forwarding an
|
|
311
|
+
optional: `{ ringCount: opts.rings }` with `rings` unset) raised an `analysis-option-corrected`
|
|
312
|
+
advisory, and that one is not quiet: it reaches the toolbar's warning indicator and the Summary
|
|
313
|
+
panel's banner. Undefined keys are now dropped before merging, so "not passed" and "passed as
|
|
314
|
+
undefined" mean the same thing. Genuinely bad values are still corrected and still reported.
|
|
315
|
+
- **`buildWaferMap({ testDefs: [] })` silently lost every value plot mode** — and every test
|
|
316
|
+
column in the die list. An empty array is load-bearing downstream (`renderWaferGallery`'s
|
|
317
|
+
reconciled "kept nothing", which no fallback may override by discovering bare test numbers back
|
|
318
|
+
from the dies), but from a HOST it just means "I described none", which is the same statement
|
|
319
|
+
as omitting the field. It is now normalised to `undefined` at that one input boundary; the
|
|
320
|
+
gallery's own signal is unaffected.
|
|
321
|
+
- **The findings expand chevron stayed painted as hovered** after the first hover:
|
|
322
|
+
`wireControlHover` was called on it twice, and the second call snapshotted the first's hover
|
|
323
|
+
colours as the resting state.
|
|
324
|
+
- **Geometry advisories rendered as truncated prose in the collapsed warnings banner.**
|
|
325
|
+
`SHORT_LABEL` still keyed the removed `inferred-pitch` and had no entry for
|
|
326
|
+
`non-standard-diameter` or `diameter-exceeds-die-extent` — the codes that replaced it — so
|
|
327
|
+
every advisory 0.27.0 actually raises fell through to a 57-character truncation of its own
|
|
328
|
+
message. Entries added for all current codes, and a test now checks the table against the
|
|
329
|
+
declared code union so it cannot drift again.
|
|
330
|
+
- **The chrome row painted an empty band** above a map rendered with `showToolbar: false` and no
|
|
331
|
+
identity: it is built unconditionally and its own comment claimed it "collapses to nothing when
|
|
332
|
+
it holds neither", but nothing implemented that. It now hides when it holds nothing visible —
|
|
333
|
+
hidden children included, since `setIdentityVisible(false)` hides in place.
|
|
334
|
+
- **Gallery metadata-mode colours could disagree with the legend beside them.** Each card
|
|
335
|
+
assigned colours by index into the values on its OWN dies, while the shared legend strip
|
|
336
|
+
ranked the union across every card — so a wafer that never exhibited one category shifted
|
|
337
|
+
every later category up a colour and painted it in the colour the legend gave to another.
|
|
338
|
+
New `ViewOptions.metadataValueOrder` (`{ key, values }`, applied only when `key` matches
|
|
339
|
+
`activeMetadataKey`, mirroring `valueRange`'s `{ test, range }` guard) is forwarded by
|
|
340
|
+
`renderWaferMap` and set automatically by `renderWaferGallery`, so every card colours from one
|
|
341
|
+
lot-wide list. Values a card has but the list doesn't are appended, never dropped.
|
|
342
|
+
- The ordering is now one implementation (`collectMetadataValues`) shared by the maps and the
|
|
343
|
+
legend, and the legend stopped deriving its values with `String(raw)` where the maps use
|
|
344
|
+
`metadataCategoricalValue` — a second divergence in the same place, visible on numeric
|
|
345
|
+
metadata.
|
|
346
|
+
- A standalone `renderWaferMap` is unchanged: with no order supplied it still derives one from
|
|
347
|
+
its own dies, which is correct for one wafer.
|
|
348
|
+
- **The histogram resolved an unset "axis includes limits" preference two different ways.**
|
|
349
|
+
Grouped (faceted) treated it as off; ungrouped derived it from the data. The same test with no
|
|
350
|
+
preference therefore included the spec limits in the axis in one view and not the other — the
|
|
351
|
+
range moved under the reader while the toggle stayed put. Both branches now derive it from
|
|
352
|
+
their own population via `shouldIncludeLimitsByDefault`.
|
|
353
|
+
- **The histogram's "Clip outliers" toggle did nothing in the grouped view.** It was rendered
|
|
354
|
+
there but `buildTestHistogramSeries` had no way to take a clip range, so the control applied to
|
|
355
|
+
the ungrouped branch only — in exactly the view where one wild reading does the most damage,
|
|
356
|
+
compressing every group's buckets at once. The builder now takes the same optional `clip` its
|
|
357
|
+
ungrouped twin has, with the same contract (narrows where limits widen; no statistic anywhere
|
|
358
|
+
is computed from a clipped population).
|
|
359
|
+
- **A grouped histogram could kill the Insights rebuild on a real lot.** The faceted axis range
|
|
360
|
+
used `Math.min(...values)`, which passes every die value as a separate argument; ~250k values
|
|
361
|
+
(25 wafers × 10k dies) throws `RangeError: Maximum call stack size exceeded`. Replaced with
|
|
362
|
+
`testValueExtent`, a loop that also avoids materialising the pooled array. The ungrouped branch
|
|
363
|
+
had the same spread and is fixed too.
|
|
364
|
+
- **`summaryPanel: { placement: 'top' | 'bottom' }` clipped its own content silently.** Those
|
|
365
|
+
placements take a fixed 180px band with `overflowY: hidden`, which most wafers' content
|
|
366
|
+
exceeds, so the last visible row was cut mid-line with nothing indicating more existed. All
|
|
367
|
+
placements now scroll, the way `'right'`/`'left'` already did. (Whether this content suits a
|
|
368
|
+
wide-short band at all is a separate open question — see TODO.md.)
|
|
369
|
+
- **Closing and reopening Insights no longer discards the shared axis toggles.** `axisPrefs`
|
|
370
|
+
("axis includes limits" / "clip outliers") lived inside `renderDistributionsSection`, which
|
|
371
|
+
`render()` rebuilds — so a scope change, or simply toggling Insights off and on, silently
|
|
372
|
+
reverted the user's choice to the default. It now sits at tab level beside the selected test
|
|
373
|
+
and group scope, which were already lifted for the same reason. Affects the gallery too.
|
|
374
|
+
- **The gallery no longer re-renders every card on every toolbar change.**
|
|
375
|
+
`syncSharedMetadataOrder` pushed `metadataValueOrder: undefined` to each card even outside
|
|
376
|
+
`metadata` mode, and each push costs a full view rebuild. It now returns early when there is
|
|
377
|
+
nothing to change, matching `syncSharedValueRange`.
|
|
378
|
+
- **`renderWaferMap`'s Insights view no longer jumps back to the top on reopen.** The tab's
|
|
379
|
+
scroll offset is remembered on close and re-applied over a short window after the rebuild —
|
|
380
|
+
one assignment is not enough, because the cards grow to their measured content over the
|
|
381
|
+
following frames, so the first write is clamped to a container that is still short (212px of
|
|
382
|
+
a requested 300px, measured) and the layout pass can then reset it to 0. A position past the
|
|
383
|
+
target stops the retries: that is the user scrolling, and they own it from then on.
|
|
384
|
+
(Gallery-side, the host page owns the scroller, so there is nothing for the library to
|
|
385
|
+
restore.)
|
|
386
|
+
- Corrected `charts/boxplot.ts`'s header comment, which still said click-to-open-wafer was
|
|
387
|
+
unimplemented long after it shipped.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
25
391
|
## [0.27.0] — 2026-09-09
|
|
26
392
|
|
|
27
393
|
### Breaking
|
package/README.md
CHANGED
|
@@ -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 **14** 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.
|
|
@@ -6,7 +6,6 @@ export interface BinClusterPanelOptions {
|
|
|
6
6
|
key: string;
|
|
7
7
|
items: BinItem[];
|
|
8
8
|
}[];
|
|
9
|
-
colorScheme?: string;
|
|
10
9
|
onSaveImage?: SaveImageHandler;
|
|
11
10
|
/** Document to build this panel's DOM into. Default `document` — pass the
|
|
12
11
|
* host's own `ownerDocument` when the container might live in a
|
|
@@ -11,7 +11,6 @@ export interface BoxplotPanelOptions {
|
|
|
11
11
|
items: BoxplotPanelItem[];
|
|
12
12
|
testDefs: TestDef[];
|
|
13
13
|
selectedTestNumber?: number;
|
|
14
|
-
colorScheme?: string;
|
|
15
14
|
onSaveImage?: SaveImageHandler;
|
|
16
15
|
/**
|
|
17
16
|
* When the Analysis tab's "Group by" is active, this panel shows one
|
|
@@ -44,6 +43,14 @@ export interface BoxplotPanelOptions {
|
|
|
44
43
|
* same test in value mode instead of defaulting to hard-bin mode.
|
|
45
44
|
* Never called for a pooled group-overview row (that drills instead). */
|
|
46
45
|
onOpen?: (waferIndex: number, testNumber: number) => void;
|
|
46
|
+
/** What `onOpen` will actually do, in the user's words — substituted into
|
|
47
|
+
* both click affordances ("click a box to …" / "click to …"). Default
|
|
48
|
+
* wording describes opening that wafer, which is what a gallery host does;
|
|
49
|
+
* a single-wafer host, where there is no other wafer to open and the click
|
|
50
|
+
* instead shows the selected test on the map it already has, passes its own
|
|
51
|
+
* (see `InsightsTabDeps.focusTest`). One string rather than two because the
|
|
52
|
+
* two affordances describe one action — if they can disagree, they will. */
|
|
53
|
+
openActionLabel?: string;
|
|
47
54
|
/** Document to build this panel's DOM into. Default `document` — pass the
|
|
48
55
|
* host's own `ownerDocument` when the container might live in a
|
|
49
56
|
* different document (e.g. a gallery card detached into its own popup
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildTestBoxplotData as
|
|
1
|
+
import{buildTestBoxplotData as Je}from"../../stats/boxplot.js";import{SPACE as Be,fontPx as X,FONT as Ze,CLR as et}from"../toolbar.js";import{fmt as de}from"../../renderer/fmt.js";import{QUANTITY as De}from"./palette.js";import{cardShell as tt,observeResize as nt,makeTooltip as ot,positionChartTooltip as it,makeBackButton as lt,makeLinkedTestSelect as st,makeToggle as at,makeLinkedAxisPrefs as rt,renderEmptyState as Ie,growCardToFitContent as ct,resolveChartCanvasColors as ft,makeAxisFormat as ut,resolveAxisRange as mt,shouldIncludeLimitsByDefault as dt,drawOffAxisLimits as xt,limitLabelSide as pt,PADDING as L,VALUE_WIDTH as ht,prepareCanvas as gt}from"./chartShell.js";const M=24,k=5,Re=110,bt=12,He=20;export function renderBoxplotPanel(h){const{title:J="Test value distribution",items:Oe,testDefs:xe,onSaveImage:We,groups:m,groupLabelText:Z="group",onOpen:N}=h,Ee=h.openActionLabel??"open that wafer",Pe=h.openActionLabel??"open this wafer",{card:a,heading:_e,body:d,controlsRow:q}=tt(J,We,h.ownerDocument);a.style.flex="0 0 auto",d.style.flex="0 0 auto",a.style.alignSelf="start";const ee=xe.filter(n=>n.testNumber!==void 0);let v=h.selectedTestNumber??ee[0]?.testNumber??null,te=!1,Y=0,x=null,I=null,ne=null,pe=null;function Ge(){const n=!m||m.length===0?"":x??"\0overview";if(ne&&pe===n)return ne;const u=!m||m.length===0?Oe:x!==null?m.find(g=>g.key===x)?.items??[]:m.map(g=>({label:g.key,dies:g.items.flatMap(c=>c.dies??[])}));return ne=u,pe=n,u}function he(){x!==null&&!I?(I=lt(()=>{ge(null),h.onGroupChange?.(null)},a.ownerDocument),q.appendChild(I)):x===null&&I&&(I.remove(),I=null),_e.textContent=x!==null?`${J} \u2014 ${Z}: ${x}`:J}function ge(n){return x===n||n!==null&&!(m??[]).some(u=>u.key===n)?!1:(x=n,he(),Q(),C(),!0)}const be=st(ee,v,n=>{v=n,C(),h.onTestChange?.(n)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Xe=be.el;q.appendChild(Xe),q.appendChild(at("Log scale",te,n=>{te=n,C()},a.ownerDocument));const oe=a.ownerDocument.createElement("span");Object.assign(oe.style,{display:"inline-flex",gap:Be.lg,alignItems:"center"}),q.appendChild(oe);const ie=rt(oe,h.axisPrefs,n=>{h.onAxisPrefsChange?.(n),C()},a.ownerDocument),le=a.ownerDocument.createElement("div");Object.assign(le.style,{color:et.label,fontSize:Ze.body,marginBottom:Be.sm}),a.insertBefore(le,d);function Q(){const n=!!m&&m.length>0&&x===null,u=[];n?u.push(`click a ${Z}'s box to see it by wafer`):N&&u.push(`click a box to ${Ee}`);const g=u.length?`${u[0][0].toUpperCase()}${u[0].slice(1)} \xB7 `:"";le.textContent=`${g}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(Y?` \xB7 axis clipped, ${Y} value${Y===1?"":"s"} outside`:"")}Q(),he();const H=ot(a);let se=null;function C(){const{includeLimits:n,clipOutliers:u}=ie.get();if(Q(),d.innerHTML="",ee.length===0||v===null){Ie(d,"No parametric test data available for box plots.");return}const g=Ge(),c=Je(g,v),ae=xe.find(t=>t.testNumber===v),U=ae?.unit,z=ae?.limitLow,F=ae?.limitHigh;if(c.every(t=>t.count===0)){Ie(d,"No parametric test data available for box plots.");return}const p=a.ownerDocument.createElement("canvas");p.style.display="block",p.style.cursor="default",d.appendChild(p);const Te=L*2+Math.min(c.length,bt)*(M+k)+He;d.style.maxHeight=`${Te}px`,d.style.overflowY="auto",d.style.scrollbarGutter="stable";let O=-1;const re=c.filter(t=>t.count>0),ye=Math.min(...re.map(t=>t.min)),ve=Math.max(...re.map(t=>t.max)),we=n??dt(ye,ve,z,F);ie.sync(we,z!==void 0||F!==void 0);const W=mt({dataMin:ye,dataMax:ve,limitLow:z,limitHigh:F,includeLimits:we,clipOutliers:u,values:re.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});Y=W.clippedCount,Q();const R=W.lo,ce=W.hi,Le=ce-R||1,V=te&&R>0,fe=V?Math.log10(R):0,Me=(V?Math.log10(ce):0)-fe||1,ue=ut(Math.max(Math.abs(R),Math.abs(ce)),U);function Ye(){const t=L+Re,i=p.clientWidth-t-ht-L;return{plotX:t,plotMaxWidth:Math.max(10,i)}}function S(t,i,s){return V?i+(Math.log10(t)-fe)/Me*s:i+(t-R)/Le*s}function Qe(){const t=[];for(let i=0;i<=4;i++)t.push(V?Math.pow(10,fe+Me*i/4):R+Le*i/4);return t}function j(){const t=ft(a),i=d.clientWidth,s=L*2+c.length*(M+k)+He,A=gt(p,a,i,s);if(!A)return;const{ctx:e}=A;e.font=`${X(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:f}=Ye();c.forEach((o,b)=>{const y=L+b*(M+k),r=y+M/2;b===O&&(e.fillStyle=t.bgHover,e.fillRect(0,y-k/2,i,M+k)),e.fillStyle=t.text,e.textAlign="right";const ze=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(ze,L+Re-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const Fe=l+f,P=Ke=>Math.min(Fe,Math.max(l,Ke)),Se=S(o.min,l,f),Ae=S(o.max,l,f),$=P(Se),_=P(S(o.q1,l,f)),$e=P(S(o.median,l,f)),me=P(S(o.q3,l,f)),B=P(Ae),Ve=Se<$,je=Ae>B,D=y+3,G=y+M-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo($,r),e.lineTo(_,r),e.moveTo(me,r),e.lineTo(B,r);const w=3;Ve?(e.moveTo($+w,r-w),e.lineTo($,r),e.lineTo($+w,r+w)):(e.moveTo($,D),e.lineTo($,G)),je?(e.moveTo(B-w,r-w),e.lineTo(B,r),e.lineTo(B-w,r+w)):(e.moveTo(B,D),e.lineTo(B,G)),e.stroke(),e.fillStyle=De,e.globalAlpha=.35,e.fillRect(_,D,Math.max(1,me-_),G-D),e.globalAlpha=1,e.strokeStyle=De,e.strokeRect(_,D,Math.max(1,me-_),G-D),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo($e,D),e.lineTo($e,G),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(de(o.median,U,"engineering"),l+f+10,r)});const T=L+c.length*(M+k);e.font=`${X(-1)}px system-ui, sans-serif`,xt(e,W.offAxis,{left:l,right:l+f,top:0,bottom:T},"horizontal",t.limitLine,o=>de(o,U,"engineering"));const Ue=new Set(W.offAxis.map(o=>o.value));for(const[o,b]of[[z,"LSL"],[F,"USL"]]){if(o===void 0||Ue.has(o))continue;const y=S(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=pt(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=`${X(-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 Qe()){const b=S(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=`${X(-1)}px system-ui, sans-serif`,e.fillText(`(${ue.unitLabel})`,l+f+22,T+6),e.font=`${X(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function ke(t){const i=Math.floor((t-L+k/2)/(M+k));return i>=0&&i<c.length?i:-1}function E(t){return de(t,U,"engineering")}const K=!!m&&m.length>0&&x===null,Ce=t=>!K&&!!N&&g[t]?.waferIndex!==void 0;p.addEventListener("mousemove",t=>{const i=p.getBoundingClientRect(),s=ke(t.clientY-i.top),A=s>=0&&c[s].count>0&&(K||Ce(s));if(s!==O&&(O=s,p.style.cursor=A?"pointer":"default",j()),s>=0&&c[s].count>0){const e=c[s],l=K?`<br><em>click to see this ${Z} by wafer</em>`:Ce(s)?`<br><em>click to ${Pe}</em>`:"";H.innerHTML=`<strong>${e.label}</strong> (${e.count} dies)<br>max ${E(e.max)}<br>q3 ${E(e.q3)}<br>median ${E(e.median)}<br>q1 ${E(e.q1)}<br>min ${E(e.min)}${l}`,H.style.display="block",it(H,a,t.clientX,t.clientY)}else H.style.display="none"}),p.addEventListener("mouseleave",()=>{O!==-1&&(O=-1,j()),H.style.display="none"}),p.addEventListener("click",t=>{const i=p.getBoundingClientRect(),s=ke(t.clientY-i.top);if(s===-1||c[s].count===0)return;if(K){const e=c[s].label;ge(e),h.onGroupChange?.(e);return}const A=g[s]?.waferIndex;N&&A!==void 0&&v!==null&&N(A,v)}),se?.disconnect(),se=nt(a,()=>j()),j(),ct(a,d,Te)}C();function Ne(n){be.set(n)&&(v=n,C())}function qe(n){ie.set(n)&&C()}return{card:a,setTest:Ne,setAxisPrefs:qe,destroy:()=>se?.disconnect()}}
|
|
@@ -12,7 +12,6 @@ export interface HistogramPanelOptions {
|
|
|
12
12
|
items: HistogramItem[];
|
|
13
13
|
testDefs: TestDef[];
|
|
14
14
|
selectedTestNumber?: number;
|
|
15
|
-
colorScheme?: string;
|
|
16
15
|
onSaveImage?: SaveImageHandler;
|
|
17
16
|
/**
|
|
18
17
|
* When the Analysis tab's "Group by" is active, this panel switches to an
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildTestHistogramData as Kt,collectTestValues as Ot,buildTestHistogramSeries as Rt}from"../../stats/histogram.js";import{SPACE as et,fontPx as X,FONT as vt,CLR as wt}from"../toolbar.js";import{fmt as ht}from"../../renderer/fmt.js";import{QUANTITY as Ft,categorical as zt}from"./palette.js";import{cardShell as Gt,observeResize as kt,makeTooltip as Vt,attachChartTip as jt,positionChartTooltip as Ht,makeLinkedTestSelect as _t,makeWaferSelect as Ut,makeLinkedAxisPrefs as Xt,renderEmptyState as yt,chartFillHeight as At,applyCanvasFlow as Ct,makeAxisFormat as $t,PADDING as at,robustFence as Yt,shouldIncludeLimitsByDefault as Qt,drawOffAxisLimits as qt,resolveAxisRange as Jt,chartSwatchCss as Zt,makeSeriesLegendItem as te,prepareCanvas as Pt}from"./chartShell.js";const Mt=230,Wt=36,gt=18;function Dt(s,N,Y,Q,ft,E,a,y=4){const q=Math.max(1,E/y),nt=Math.pow(10,Math.floor(Math.log10(q))),g=q/nt,J=(g<=1?1:g<=2?2:g<=5?5:10)*nt,Z=Q-Y;s.save(),s.font=`${X(-1)}px system-ui, sans-serif`,s.textAlign="right",s.textBaseline="middle";for(let K=0;K<=E+1e-9;K+=J){const ot=Q-K/(E||1)*Z;s.strokeStyle=a.grid,s.lineWidth=1,s.beginPath(),s.moveTo(N,ot),s.lineTo(N+ft,ot),s.stroke(),s.fillStyle=a.text,s.fillText(String(Math.round(K)),N-4,ot)}s.strokeStyle=a.axis,s.lineWidth=1,s.beginPath(),s.moveTo(N,Y),s.lineTo(N,Q),s.stroke(),s.restore()}export function renderHistogramPanel(s){const{title:N="Test value distribution",items:Y,testDefs:Q,onSaveImage:ft,groups:E}=s,{card:a,body:y,controlsRow:q}=Gt(N,ft,s.ownerDocument),nt=Q.filter(l=>l.testNumber!==void 0);let g=s.selectedTestNumber??nt[0]?.testNumber??null,J=null,Z=0;const K=_t(nt,g,l=>{g=l,O(),s.onTestChange?.(l)},{maxWidth:"200px",ownerDocument:a.ownerDocument}),ot=K.el;q.appendChild(ot);const Tt=Ut(Y,J,l=>{J=l,O()},{ownerDocument:a.ownerDocument});q.appendChild(Tt);function Et(l){return L===l||l!==null&&!(E??[]).some(A=>A.key===l)?!1:(L=l,O(),!0)}const pt=a.ownerDocument.createElement("span");Object.assign(pt.style,{display:"inline-flex",gap:et.lg,alignItems:"center"}),q.appendChild(pt);const rt=Xt(pt,s.axisPrefs,l=>{s.onAxisPrefsChange?.(l),O()},a.ownerDocument),H=Vt(a);let it=null,L=null;function lt(l){const A=Q.find(B=>B.testNumber===l);return{unit:A?.unit,limitLow:A?.limitLow,limitHigh:A?.limitHigh}}function O(){const{includeLimits:l,clipOutliers:A}=rt.get();if(it?.disconnect(),it=null,y.innerHTML="",g===null){yt(y,"No parametric test data available for a histogram.");return}const B=E&&E.length>0?Rt(E,g,16,l?lt(g).limitLow:void 0,l?lt(g).limitHigh:void 0):null;if(Tt.style.display=B?"none":"",B){const{limitLow:n,limitHigh:i}=lt(g);if(rt.sync(l??!1,n!==void 0||i!==void 0),B.series.length===0){yt(y,"No parametric test data available for a histogram.");return}Bt(B);return}const ct=J!==null?[Y[J]]:Y,{unit:f,limitLow:w,limitHigh:D}=lt(g),C=Ot(ct,g),R=C.length?Math.min(...C):NaN,F=C.length?Math.max(...C):NaN,v=l??Qt(R,F,w,D);rt.sync(v,w!==void 0||D!==void 0);const z=A?Yt(C):null,G=z?{lo:Math.max(R,z.lo),hi:Math.min(F,z.hi)}:void 0;Z=G?C.filter(n=>n<G.lo||n>G.hi).length:0;const u=Kt(ct,g,16,v?w:void 0,v?D:void 0,G);if(u.length===0){yt(y,"No parametric test data available for a histogram.");return}const V=Math.max(...u.map(n=>n.count),1),j=a.ownerDocument.createElement("div");Object.assign(j.style,{fontSize:vt.body,color:wt.label,marginBottom:et.xxs}),j.textContent=`max ${V} dies/bucket`+(Z?` \xB7 ${Z} value${Z===1?"":"s"} outside clipped range`:""),y.appendChild(j);const k=a.ownerDocument.createElement("canvas");k.style.display="block",k.style.cursor="default",y.appendChild(k);let S=-1;const _=u[0].rangeLow,I=u[u.length-1].rangeHigh,xt=I-_||1,o=$t(Math.max(Math.abs(_),Math.abs(I)),f);function p(n){const i=at+36,r=k.clientWidth-i-at,e=n-Wt-gt;return{plotX:i,plotMaxWidth:Math.max(10,r),plotMaxHeight:e,plotTop:gt}}function M(n){const{plotX:i,plotMaxWidth:r}=p(Mt);if(n<i||n>i+r)return-1;const e=Math.floor((n-i)/r*u.length);return e>=0&&e<u.length?e:-1}function t(){Ct(k,j);const n=y.clientWidth,i=At(a,y,k,Mt),r=Pt(k,a,n,i);if(!r)return;const{ctx:e,theme:x}=r,{plotX:c,plotMaxWidth:b,plotMaxHeight:dt,plotTop:U}=p(i),$=U+dt,tt=b/u.length;Dt(e,c,U,$,b,V,{text:x.textMuted,axis:x.border,grid:x.border}),u.forEach((m,T)=>{const h=m.count/V*dt,Lt=c+T*tt,St=$-h;e.fillStyle=T===S?x.text:Ft,e.globalAlpha=T===S?1:.7,e.fillRect(Lt+1,St,Math.max(1,tt-2),h),e.globalAlpha=1,T===S&&(e.fillStyle=x.text,e.textAlign="center",e.textBaseline="bottom",e.fillText(`${m.count}`,Lt+tt/2,St-2))});const bt=m=>c+(m-_)/xt*b;e.font=`${X(-1)}px system-ui, sans-serif`;const{offAxis:d}=Jt({dataMin:_,dataMax:I,limitLow:w,limitHigh:D,includeLimits:!1});qt(e,d,{left:c,right:c+b,top:U,bottom:$},"horizontal",x.limitLine,o.tick);const P=new Set(d.map(m=>m.value));for(const[m,T]of[[w,"LSL"],[D,"USL"]]){if(m===void 0||P.has(m))continue;const h=bt(m);e.strokeStyle=x.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(h,U),e.lineTo(h,$),e.stroke(),e.setLineDash([]),e.fillStyle=x.limitLine,e.textAlign="center",e.textBaseline="bottom",e.fillText(T,h,U-2)}e.font=`${X(-1)}px system-ui, sans-serif`,e.strokeStyle=x.border,e.lineWidth=1,e.beginPath(),e.moveTo(c,$),e.lineTo(c+b,$),e.stroke(),e.fillStyle=x.textMuted,e.textAlign="center",e.textBaseline="top";const mt=Math.max(2,Math.floor(b/44)),ut=Math.ceil(u.length/mt),st=Math.max(1,ut);for(let m=0;m<=u.length;m+=st){const T=m<u.length?u[m].rangeLow:u[u.length-1].rangeHigh,h=c+m*tt;e.beginPath(),e.moveTo(h,$),e.lineTo(h,$+4),e.stroke(),e.fillText(o.tick(T),h,$+6)}o.unitLabel&&(e.save(),e.font=`${X(-1)}px system-ui, sans-serif`,e.fillStyle=x.textMuted,e.textAlign="right",e.textBaseline="top",e.fillText(`(${o.unitLabel})`,c+b,$+20),e.restore())}k.addEventListener("mousemove",n=>{const i=k.getBoundingClientRect(),r=M(n.clientX-i.left);if(r!==S&&(S=r,t()),r>=0){const e=u[r];H.innerHTML=`<strong>${ht(e.rangeLow,f,"engineering")} \u2013 ${ht(e.rangeHigh,f,"engineering")}</strong><br>${e.count} dies`,H.style.display="block",Ht(H,a,n.clientX,n.clientY)}else H.style.display="none"}),k.addEventListener("mouseleave",()=>{S!==-1&&(S=-1,t()),H.style.display="none"}),it=kt(a,()=>t()),t()}function Bt(l){const{unit:A,limitLow:B,limitHigh:ct}=lt(g),f=l.ranges,w=l.series,D=zt;L&&!w.some(o=>o.groupKey===L)&&(L=null);const C=Math.max(1,...w.flatMap(o=>o.counts)),R=a.ownerDocument.createElement("div");Object.assign(R.style,{fontSize:vt.body,color:wt.label,marginBottom:et.xxs}),R.textContent=`${w.length} groups \xB7 max ${C} dies/bucket`,y.appendChild(R);const F=a.ownerDocument.createElement("div");Object.assign(F.style,{display:"flex",flexWrap:"wrap",gap:et.sm,marginTop:et.md,marginBottom:et.xl}),w.forEach((o,p)=>{const M=te(o.groupKey,D(p),a.ownerDocument),t=M.el;jt(t,a,H,`${o.groupKey} \u2014 click to emphasize (dim the rest)`),M.setState({selected:L===o.groupKey,dimmed:L!==null&&L!==o.groupKey}),t.addEventListener("click",()=>{const n=L===o.groupKey?null:o.groupKey;Et(n),s.onGroupChange?.(n)}),F.appendChild(t)}),y.appendChild(F);const v=a.ownerDocument.createElement("canvas");v.style.display="block",v.style.cursor="default",y.appendChild(v);const z=f[0].rangeLow,G=f[f.length-1].rangeHigh,u=G-z||1,V=$t(Math.max(Math.abs(z),Math.abs(G)),A),j=o=>{const p=(o.ownerDocument.defaultView??window).getComputedStyle(o);return o.offsetHeight+(parseFloat(p.marginTop)||0)+(parseFloat(p.marginBottom)||0)},k=()=>j(R)+j(F);let S=-1,_={plotX:at+36,plotMaxWidth:10,barWidth:10};function I(){Ct(v,k());const o=y.clientWidth,p=At(a,y,v,Mt),M=Pt(v,a,o,p);if(!M)return;const{ctx:t,theme:n}=M,i=at+36,r=Math.max(10,o-i-at),e=gt,x=p-Wt-gt,c=e+x,b=r/f.length;_={plotX:i,plotMaxWidth:r,barWidth:b},Dt(t,i,e,c,r,C,{text:n.textMuted,axis:n.border,grid:n.border}),S>=0&&(t.fillStyle=n.bgHover,t.globalAlpha=.5,t.fillRect(i+S*b,e,b,x),t.globalAlpha=1);const dt=w.map((d,P)=>({s:d,i:P})).sort((d,P)=>(d.s.groupKey===L?1:0)-(P.s.groupKey===L?1:0));for(const{s:d,i:P}of dt){const W=L===d.groupKey,mt=L!==null&&!W,ut=D(P);t.strokeStyle=ut,t.fillStyle=ut,t.lineWidth=W?2:1,t.globalAlpha=mt?.12:L?.32:.22,t.beginPath(),t.moveTo(i,c),d.counts.forEach((st,m)=>{const T=i+m*b,h=c-st/C*x;t.lineTo(T,h),t.lineTo(T+b,h)}),t.lineTo(i+r,c),t.closePath(),t.fill(),t.globalAlpha=mt?.3:1,t.beginPath(),d.counts.forEach((st,m)=>{const T=i+m*b,h=c-st/C*x;m===0?t.moveTo(T,h):t.lineTo(T,h),t.lineTo(T+b,h)}),t.stroke()}t.globalAlpha=1;const U=d=>i+(d-z)/u*r;t.font=`${X(-1)}px system-ui, sans-serif`;for(const[d,P]of[[B,"LSL"],[ct,"USL"]]){if(d===void 0)continue;const W=U(d);t.strokeStyle=n.limitLine,t.setLineDash([3,3]),t.lineWidth=1,t.beginPath(),t.moveTo(W,e),t.lineTo(W,c),t.stroke(),t.setLineDash([]),t.fillStyle=n.limitLine,t.textAlign="center",t.textBaseline="bottom",t.fillText(P,W,e-2)}t.font=`${X(-1)}px system-ui, sans-serif`,t.strokeStyle=n.border,t.lineWidth=1,t.beginPath(),t.moveTo(i,c),t.lineTo(i+r,c),t.stroke(),t.fillStyle=n.textMuted,t.textAlign="center",t.textBaseline="top";const tt=Math.max(2,Math.floor(r/44)),bt=Math.max(1,Math.ceil(f.length/tt));for(let d=0;d<=f.length;d+=bt){const P=d<f.length?f[d].rangeLow:f[f.length-1].rangeHigh,W=i+d*b;t.beginPath(),t.moveTo(W,c),t.lineTo(W,c+4),t.stroke(),t.fillText(V.tick(P),W,c+6)}V.unitLabel&&(t.save(),t.font=`${X(-1)}px system-ui, sans-serif`,t.fillStyle=n.textMuted,t.textAlign="right",t.textBaseline="top",t.fillText(`(${V.unitLabel})`,i+r,c+20),t.restore())}function xt(o){const{plotX:p,plotMaxWidth:M,barWidth:t}=_;if(o<p||o>p+M)return-1;const n=Math.floor((o-p)/t);return n>=0&&n<f.length?n:-1}v.addEventListener("mousemove",o=>{const p=v.getBoundingClientRect(),M=xt(o.clientX-p.left);if(M!==S&&(S=M,I()),M>=0){const t=f[M],n=w.map((i,r)=>`<span style="${Zt(D(r))};margin-right:4px"></span>${i.groupKey}: ${i.counts[M]}`).join("<br>");H.innerHTML=`<strong>${ht(t.rangeLow,A,"engineering")} \u2013 ${ht(t.rangeHigh,A,"engineering")}</strong><br>${n}`,H.style.display="block",Ht(H,a,o.clientX,o.clientY)}else H.style.display="none"}),v.addEventListener("mouseleave",()=>{S!==-1&&(S=-1,I()),H.style.display="none"}),it=kt(a,()=>I()),I()}O();function It(l){K.set(l)&&(g=l,O())}function Nt(l){rt.set(l)&&O()}return{card:a,setTest:It,setAxisPrefs:Nt,destroy:()=>it?.disconnect()}}
|
|
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()}}
|
|
@@ -6,7 +6,6 @@ export interface RegionYieldDiagramOptions {
|
|
|
6
6
|
mode: RegionYieldMode;
|
|
7
7
|
/** Ring mode: ordered ring 1 (core) → ring N (edge), matching `buildRegionYieldData`'s output for `buildRingRegions`. Quadrant mode: any order — each row's quadrant is read from its `key` (`quadrant:NE` etc.), not position. */
|
|
8
8
|
rows: RegionYieldDatum[];
|
|
9
|
-
colorScheme?: string;
|
|
10
9
|
onSaveImage?: SaveImageHandler;
|
|
11
10
|
/** Document to build this panel's DOM into. Default `document` — pass the
|
|
12
11
|
* host's own `ownerDocument` when the container might live in a
|
|
@@ -11,7 +11,12 @@ export interface ScatterPanelOptions {
|
|
|
11
11
|
testDefs: TestDef[];
|
|
12
12
|
xTestNumber?: number;
|
|
13
13
|
yTestNumber?: number;
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The host map's resolved hard-bin colours (`View.binColors.hard`), so a
|
|
16
|
+
* point is the colour its die is on the map. Omitted ⇒ points without a
|
|
17
|
+
* group colour take the no-data fill.
|
|
18
|
+
*/
|
|
19
|
+
binColors?: ReadonlyMap<number, string>;
|
|
15
20
|
onSaveImage?: SaveImageHandler;
|
|
16
21
|
/**
|
|
17
22
|
* When the Analysis tab's "Group by" is active, every group's points are
|