@wafertools/wafermap 0.26.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +360 -0
- package/README.md +46 -3
- package/dist/packages/canvas-adapter/canvasTheme.d.ts +4 -0
- package/dist/packages/canvas-adapter/canvasTheme.js +1 -1
- package/dist/packages/canvas-adapter/charts/barPanel.d.ts +16 -0
- 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.d.ts +13 -1
- package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
- package/dist/packages/canvas-adapter/charts/capability.d.ts +10 -10
- package/dist/packages/canvas-adapter/charts/capability.js +1 -1
- package/dist/packages/canvas-adapter/charts/chartShell.d.ts +337 -3
- package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
- package/dist/packages/canvas-adapter/charts/correlation.d.ts +8 -9
- package/dist/packages/canvas-adapter/charts/correlation.js +2 -1
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +50 -0
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -0
- package/dist/packages/canvas-adapter/charts/histogram.d.ts +12 -1
- package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
- package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
- package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +20 -0
- package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -0
- package/dist/packages/canvas-adapter/charts/trend.d.ts +30 -0
- package/dist/packages/canvas-adapter/charts/trend.js +1 -0
- package/dist/packages/canvas-adapter/dieList.js +7 -7
- package/dist/packages/canvas-adapter/identityHeader.d.ts +46 -0
- package/dist/packages/canvas-adapter/identityHeader.js +1 -0
- package/dist/packages/canvas-adapter/index.d.ts +1 -1
- package/dist/packages/canvas-adapter/insightsTab.d.ts +27 -3
- package/dist/packages/canvas-adapter/insightsTab.js +1 -1
- package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +15 -2
- package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -20
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +72 -17
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +219 -33
- package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
- package/dist/packages/canvas-adapter/toCanvas.d.ts +6 -0
- package/dist/packages/canvas-adapter/toCanvas.js +1 -1
- package/dist/packages/canvas-adapter/toolbar.d.ts +310 -6
- 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 +97 -34
- 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 +0 -4
- package/dist/packages/canvas-adapter/warnings.js +1 -1
- package/dist/packages/core/utils.d.ts +11 -0
- package/dist/packages/core/utils.js +1 -1
- package/dist/packages/renderer/buildView.d.ts +34 -1
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.d.ts +104 -5
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/renderer/colorSchemes.js +1 -1
- package/dist/packages/stats/analyzeWaferMap.js +1 -1
- package/dist/packages/stats/binPareto.d.ts +15 -0
- package/dist/packages/stats/binPareto.js +1 -1
- package/dist/packages/stats/clusterDetection.js +1 -1
- package/dist/packages/stats/connectedComponents.d.ts +14 -0
- package/dist/packages/stats/connectedComponents.js +1 -0
- package/dist/packages/stats/correlation.d.ts +26 -0
- package/dist/packages/stats/correlation.js +1 -1
- package/dist/packages/stats/facets.js +1 -1
- package/dist/packages/stats/filterFindings.d.ts +18 -0
- package/dist/packages/stats/filterFindings.js +1 -1
- package/dist/packages/stats/findingsNarrative.js +1 -1
- package/dist/packages/stats/histogram.d.ts +19 -1
- package/dist/packages/stats/histogram.js +1 -1
- package/dist/packages/stats/index.d.ts +6 -0
- package/dist/packages/stats/index.js +1 -1
- package/dist/packages/stats/mergeTestDefs.d.ts +44 -0
- package/dist/packages/stats/mergeTestDefs.js +1 -0
- package/dist/packages/stats/metadataColumns.d.ts +20 -4
- package/dist/packages/stats/metadataColumns.js +1 -1
- package/dist/packages/stats/patternClassification.js +1 -1
- package/dist/packages/stats/renderFindingsReport.js +14 -14
- package/dist/packages/stats/renderSummaryReport.js +32 -29
- package/dist/packages/stats/reportHtml.js +38 -13
- package/dist/packages/stats/testPassRate.d.ts +85 -0
- package/dist/packages/stats/testPassRate.js +1 -0
- package/dist/packages/stats/trend.d.ts +40 -0
- package/dist/packages/stats/trend.js +1 -0
- package/dist/packages/stats/types.d.ts +117 -6
- package/llms.txt +6 -0
- package/package.json +7 -5
- package/dist/packages/canvas-adapter/metadataBadge.d.ts +0 -30
- package/dist/packages/canvas-adapter/metadataBadge.js +0 -1
|
@@ -17,6 +17,21 @@ export interface ChartCanvasColors {
|
|
|
17
17
|
/** Subtle bar/row "track" background (full-extent backdrop behind a value bar) — deliberately much softer than `border`, which reads as a visible grey line/fill, not a backdrop. */
|
|
18
18
|
track: string;
|
|
19
19
|
warnBorder: string;
|
|
20
|
+
/** Selection accent — the "this one is chosen" colour, distinct from
|
|
21
|
+
* `bgHover`, which is "the pointer is over this one". */
|
|
22
|
+
accent: string;
|
|
23
|
+
/**
|
|
24
|
+
* Spec-limit reference lines and their labels.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately NOT `warnBorder`, which is what these used. That token is the
|
|
27
|
+
* border of a warning banner — designed to sit quietly on a warning
|
|
28
|
+
* background, so every dark theme sets it to a near-black amber (`#3a2a00`,
|
|
29
|
+
* `#4a3f2a`, …). Drawn instead as a line across a chart surface it was barely
|
|
30
|
+
* visible in every theme, light included. `warn-text` is the token in the
|
|
31
|
+
* same family that must be READABLE against a panel, which is exactly what a
|
|
32
|
+
* limit line and its label need.
|
|
33
|
+
*/
|
|
34
|
+
limitLine: string;
|
|
20
35
|
}
|
|
21
36
|
/** Resolve `--wmap-*` custom properties to concrete color strings for canvas
|
|
22
37
|
* drawing. Call once per draw (not cached) so a live theme change is picked
|
|
@@ -71,7 +86,52 @@ export declare function growCardToFitContent(card: HTMLElement, body: HTMLElemen
|
|
|
71
86
|
* actually deliver that floor.
|
|
72
87
|
*/
|
|
73
88
|
export declare function chartFillHeight(card: HTMLElement, body: HTMLElement, canvas: HTMLCanvasElement, minHeight: number): number;
|
|
74
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Position a chart canvas to fill its card below whatever sits above it.
|
|
91
|
+
*
|
|
92
|
+
* Pass the ELEMENT above, not a number. Callers passed `el.offsetHeight`, which
|
|
93
|
+
* excludes margins — so the canvas began at that element's content edge and any
|
|
94
|
+
* margin on it became overlap. The scatter legend's chips were drawn over by the
|
|
95
|
+
* plot for exactly this reason, and the overlap grew the moment the legend was
|
|
96
|
+
* given the breathing room it needed. A number is still accepted for a caller
|
|
97
|
+
* that genuinely has only a measurement, but an element is always safer: it
|
|
98
|
+
* cannot omit the part that made this wrong.
|
|
99
|
+
*/
|
|
100
|
+
/**
|
|
101
|
+
* Device pixel ratio of the window `el` actually lives in.
|
|
102
|
+
*
|
|
103
|
+
* Not `window.devicePixelRatio`. A chart card can be reparented into a detached
|
|
104
|
+
* popup (see `renderWaferGallery`'s detach), and that popup can sit on a
|
|
105
|
+
* different display with a different ratio — reading the opener's global there
|
|
106
|
+
* sizes the backing store for the wrong screen, which shows up as a blurry or
|
|
107
|
+
* mis-scaled plot only on a multi-monitor setup. Eight of the ten charts read
|
|
108
|
+
* the global; `trend` and `testPassRate` had already worked this out
|
|
109
|
+
* separately, which is the usual sign the knowledge wants to live in one place.
|
|
110
|
+
*/
|
|
111
|
+
export declare function chartDpr(el: HTMLElement): number;
|
|
112
|
+
export interface PreparedCanvas {
|
|
113
|
+
ctx: CanvasRenderingContext2D;
|
|
114
|
+
theme: ChartCanvasColors;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Size a chart canvas for the display and hand back a cleared, scaled context
|
|
118
|
+
* plus the resolved theme.
|
|
119
|
+
*
|
|
120
|
+
* Every chart opened its draw with the same eight lines — backing store at
|
|
121
|
+
* `cssW * dpr`, CSS size in px, `getContext`, `setTransform(dpr, …)`,
|
|
122
|
+
* `clearRect`, the default font and baseline, then `resolveChartCanvasColors` —
|
|
123
|
+
* and each one was a place to get the DPR from the wrong window (eight of ten
|
|
124
|
+
* did), to forget `setTransform` after resizing, or to clear in device pixels
|
|
125
|
+
* instead of CSS ones. None of that is per-chart knowledge.
|
|
126
|
+
*
|
|
127
|
+
* Returns `null` when the 2D context is unavailable, so a caller's `if (!prep)
|
|
128
|
+
* return;` replaces the `getContext` null check it already had.
|
|
129
|
+
*
|
|
130
|
+
* The font and baseline are the common default, not a policy: a chart that
|
|
131
|
+
* wants something else assigns over them, as several do per text run anyway.
|
|
132
|
+
*/
|
|
133
|
+
export declare function prepareCanvas(canvas: HTMLCanvasElement, card: HTMLElement, cssWidth: number, cssHeight: number): PreparedCanvas | null;
|
|
134
|
+
export declare function applyCanvasFlow(canvas: HTMLCanvasElement, above?: HTMLElement | number): void;
|
|
75
135
|
/**
|
|
76
136
|
* Format a bare numeric value (no unit) for chart labels/tooltips —
|
|
77
137
|
* callers append their own unit string separately when they have one. Uses
|
|
@@ -103,10 +163,90 @@ export interface CardShell {
|
|
|
103
163
|
controlsRow: HTMLElement;
|
|
104
164
|
body: HTMLElement;
|
|
105
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* THE colour chip that stands for a data series, everywhere in the chart suite.
|
|
168
|
+
*
|
|
169
|
+
* Returned as a CSS string so an `innerHTML` row and a real element can both
|
|
170
|
+
* use it without a second copy.
|
|
171
|
+
*
|
|
172
|
+
* There were three: a 9px rounded square written as an HTML string (histogram
|
|
173
|
+
* tooltip), a 9px circle written as a style object (scatter legend), and an
|
|
174
|
+
* 11px bordered square with a dashed variant (process capability). Each was
|
|
175
|
+
* reasonable alone; together they meant the same idea looked different in every
|
|
176
|
+
* chart a reader moved between.
|
|
177
|
+
*
|
|
178
|
+
* One shape, deliberately — an earlier version made shape a parameter so a bar
|
|
179
|
+
* chart could keep a block and a scatter a dot, on the theory that a chip
|
|
180
|
+
* should mirror its mark. That is a defensible idea and it is not what these
|
|
181
|
+
* are for: they are a colour key, read across charts, and the sameness is worth
|
|
182
|
+
* more than the echo. A pill also matches `sevDot` in the Summary panel, so the
|
|
183
|
+
* app has one idiom rather than one per surface.
|
|
184
|
+
*
|
|
185
|
+
* `outline` is the single real variant: capability needs "no spec limits, no
|
|
186
|
+
* Ppk" to read as absent rather than as another colour, which a hollow dashed
|
|
187
|
+
* chip does and a filled one cannot.
|
|
188
|
+
*/
|
|
189
|
+
export declare function chartSwatchCss(color: string, variant?: 'solid' | 'outline'): string;
|
|
190
|
+
export interface SeriesLegendItem {
|
|
191
|
+
el: HTMLButtonElement;
|
|
192
|
+
/** `selected` = this series is picked; `dimmed` = a selection exists elsewhere. */
|
|
193
|
+
setState(state: {
|
|
194
|
+
selected: boolean;
|
|
195
|
+
dimmed: boolean;
|
|
196
|
+
}): void;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* THE clickable chip in a chart's series legend.
|
|
200
|
+
*
|
|
201
|
+
* There were two, built independently: the scatter's category filter and the
|
|
202
|
+
* histogram's group emphasis. Same control, same job — click a colour to narrow
|
|
203
|
+
* what the plot shows — and they differed in every particular: pill versus no
|
|
204
|
+
* border, a shared swatch versus a 10px square, `wireControlHover` versus a
|
|
205
|
+
* `filter: brightness(0.94)` that is invisible on a dark theme, and 0.35 versus
|
|
206
|
+
* 0.45 dimming.
|
|
207
|
+
*
|
|
208
|
+
* They also each hit the same trap. Both marked "selected" with a BACKGROUND,
|
|
209
|
+
* which `wireControlHover` owns: it snapshots a resting background on first
|
|
210
|
+
* hover and restores it on leave, so hovering a chip before clicking wiped the
|
|
211
|
+
* selection's look, and hovering a selected one left the look behind after it
|
|
212
|
+
* was deselected — chips displaying a state they were not in. Selection lives
|
|
213
|
+
* on `borderColor` and `fontWeight` here for that reason: neither is a channel
|
|
214
|
+
* hover touches, and weight means the cue is not colour alone.
|
|
215
|
+
*/
|
|
216
|
+
export declare function makeSeriesLegendItem(label: string, color: string, ownerDocument?: Document): SeriesLegendItem;
|
|
217
|
+
export declare function cardFrameStyle(): Partial<CSSStyleDeclaration>;
|
|
106
218
|
export declare function cardShell(title: string, onSaveImage?: SaveImageHandler, ownerDocument?: Document): CardShell;
|
|
107
|
-
export declare function makeSegmented(options: Array<[value: string, label: string]>, current: string, onChange: (value: string) => void, ownerDocument?: Document
|
|
219
|
+
export declare function makeSegmented(options: Array<[value: string, label: string]>, current: string, onChange: (value: string) => void, ownerDocument?: Document,
|
|
220
|
+
/** Smaller type/padding for the summary panel's 260px column, where the
|
|
221
|
+
* chart-card sizing overflows a section header. Charts keep the default. */
|
|
222
|
+
compact?: boolean): HTMLElement;
|
|
108
223
|
export declare function makeBackButton(onBack: () => void, ownerDocument?: Document): HTMLButtonElement;
|
|
109
224
|
export declare function renderEmptyState(body: HTMLElement, message: string, styleOverrides?: Partial<CSSStyleDeclaration>): void;
|
|
225
|
+
export interface ListSelectOption {
|
|
226
|
+
value: string;
|
|
227
|
+
label: string;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A themed single-select picker: a trigger button plus an on-demand popup
|
|
231
|
+
* listbox. Past `MENU_SEARCH_THRESHOLD` options a filter box is added (sharing
|
|
232
|
+
* `makeMenuSearchBox` with toolbar.ts's plot-mode cascade, so every long list
|
|
233
|
+
* in the library filters the same way).
|
|
234
|
+
*
|
|
235
|
+
* Exposes a settable `.value` matching `<select>.value` semantics — assigning
|
|
236
|
+
* it moves the selection WITHOUT firing `onChange`, which callers rely on to
|
|
237
|
+
* sync from an external click-through (the correlation matrix picking a cell)
|
|
238
|
+
* without re-entering their own change handler.
|
|
239
|
+
*/
|
|
240
|
+
export declare function makeListSelect(options: readonly ListSelectOption[], selected: string, onChange: (value: string) => void, opts?: {
|
|
241
|
+
maxWidth?: string;
|
|
242
|
+
ownerDocument?: Document;
|
|
243
|
+
ariaLabel?: string;
|
|
244
|
+
emptyText?: string;
|
|
245
|
+
searchPlaceholder?: string;
|
|
246
|
+
hook?: string;
|
|
247
|
+
}): HTMLElement & {
|
|
248
|
+
value: string;
|
|
249
|
+
};
|
|
110
250
|
export interface TestSelectItem {
|
|
111
251
|
testNumber: number;
|
|
112
252
|
name?: string;
|
|
@@ -129,6 +269,94 @@ export declare function makeTestSelect(testOptions: readonly TestSelectItem[], s
|
|
|
129
269
|
}): HTMLElement & {
|
|
130
270
|
value: string;
|
|
131
271
|
};
|
|
272
|
+
/** The two axis toggles plus the state behind them. */
|
|
273
|
+
export interface LinkedAxisPrefs {
|
|
274
|
+
/** Current prefs — read these where the panel used its own locals. */
|
|
275
|
+
get(): {
|
|
276
|
+
includeLimits: boolean | undefined;
|
|
277
|
+
clipOutliers: boolean;
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* Rebuild the toggles for the state the panel has just RESOLVED.
|
|
281
|
+
*
|
|
282
|
+
* Called from the redraw, because `includeLimits` is a tri-state: undefined
|
|
283
|
+
* means "decide from the data" (`shouldIncludeLimitsByDefault`), and the
|
|
284
|
+
* resolved answer is only known once the data range is. `hasLimits` decides
|
|
285
|
+
* whether the limits toggle is offered at all.
|
|
286
|
+
*/
|
|
287
|
+
sync(resolvedIncludeLimits: boolean, hasLimits: boolean): void;
|
|
288
|
+
/**
|
|
289
|
+
* Adopt prefs chosen elsewhere. Returns false — changing nothing — when they
|
|
290
|
+
* already match. Deliberately does NOT fire `onUserChange`, the same
|
|
291
|
+
* asymmetry `makeLinkedTestSelect` relies on to keep a broadcast from
|
|
292
|
+
* bouncing back.
|
|
293
|
+
*/
|
|
294
|
+
set(prefs: AxisPrefs): boolean;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The linked axis toggles shared by the boxplot, histogram and trend panels.
|
|
298
|
+
*
|
|
299
|
+
* The third copy of one behaviour, and the second to be extracted: these three
|
|
300
|
+
* each held their own `axisIncludesLimits`/`clipOutliers` pair, their own
|
|
301
|
+
* `syncAxisToggles` building the same two `makeToggle`s into a row, their own
|
|
302
|
+
* `onAxisPrefsChange` call in each handler, and a `setAxisPrefs` that was
|
|
303
|
+
* byte-identical across all three.
|
|
304
|
+
*
|
|
305
|
+
* It worked, unlike the test selector it sits beside — but only because these
|
|
306
|
+
* panels happen to rebuild their toggle row on every redraw, so an externally
|
|
307
|
+
* set value is re-rendered as a side effect. The test select was built once and
|
|
308
|
+
* never re-synced, and that is the whole difference between the two. Neither
|
|
309
|
+
* arrangement was a decision; both were where the code happened to land.
|
|
310
|
+
*/
|
|
311
|
+
export declare function makeLinkedAxisPrefs(row: HTMLElement, initial: AxisPrefs | undefined, onUserChange: (prefs: AxisPrefs) => void, ownerDocument?: Document): LinkedAxisPrefs;
|
|
312
|
+
/** A test selector plus the "which test is active" state behind it. */
|
|
313
|
+
export interface LinkedTestSelect {
|
|
314
|
+
/** The control, to append into a panel's `controlsRow`. */
|
|
315
|
+
el: HTMLElement;
|
|
316
|
+
/** Currently selected test, or null when there are none to choose. */
|
|
317
|
+
get(): number | null;
|
|
318
|
+
/**
|
|
319
|
+
* Adopt a test chosen ELSEWHERE, syncing the control to match.
|
|
320
|
+
*
|
|
321
|
+
* Returns false — changing nothing — for an unknown test or one already
|
|
322
|
+
* active. Deliberately does NOT fire `onUserChange`: that asymmetry is what
|
|
323
|
+
* makes a set of panels safe to link to each other, because a broadcast can
|
|
324
|
+
* never bounce back and start a loop.
|
|
325
|
+
*/
|
|
326
|
+
set(testNumber: number): boolean;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The linked test selector shared by the boxplot, histogram and trend panels.
|
|
330
|
+
*
|
|
331
|
+
* These each had their own copy of "an active test, a `makeTestSelect` bound to
|
|
332
|
+
* it, and a `setTest` for a sibling panel to call" — and the copies had drifted
|
|
333
|
+
* apart in exactly the way copies do. Boxplot and histogram guarded against an
|
|
334
|
+
* unknown or unchanged test and assigned `select.value` to keep the control in
|
|
335
|
+
* step; trend did neither, and could not, because it appended its select
|
|
336
|
+
* without keeping a reference. So driving trend from another chart moved its
|
|
337
|
+
* data and left its selector reading the previous test — a control stating
|
|
338
|
+
* something the panel was not showing.
|
|
339
|
+
*
|
|
340
|
+
* Notifying is part of the same behaviour and was missing everywhere: only the
|
|
341
|
+
* capability chart ever told anyone, so picking a test in any of these three
|
|
342
|
+
* silently desynchronised the other two.
|
|
343
|
+
*/
|
|
344
|
+
export declare function makeLinkedTestSelect(testOptions: readonly TestSelectItem[], initial: number | null, onUserChange: (testNumber: number) => void, opts?: {
|
|
345
|
+
maxWidth?: string;
|
|
346
|
+
emptyText?: string;
|
|
347
|
+
ownerDocument?: Document;
|
|
348
|
+
}): LinkedTestSelect;
|
|
349
|
+
export declare const ALL_GROUPS = "\0all";
|
|
350
|
+
export interface LinkedGroupSelect {
|
|
351
|
+
el: HTMLElement;
|
|
352
|
+
get: () => string | null;
|
|
353
|
+
/** Adopt a group broadcast from the section. Returns whether it changed. */
|
|
354
|
+
set: (key: string | null) => boolean;
|
|
355
|
+
}
|
|
356
|
+
export declare function makeLinkedGroupSelect(groupKeys: readonly string[], labelText: string, initial: string | null, onUserChange: (key: string | null) => void, opts?: {
|
|
357
|
+
maxWidth?: string;
|
|
358
|
+
ownerDocument?: Document;
|
|
359
|
+
}): LinkedGroupSelect;
|
|
132
360
|
export interface WaferSelectItem {
|
|
133
361
|
label?: string;
|
|
134
362
|
}
|
|
@@ -136,7 +364,9 @@ export declare function makeWaferSelect(items: readonly WaferSelectItem[], selec
|
|
|
136
364
|
maxWidth?: string;
|
|
137
365
|
allLabel?: string;
|
|
138
366
|
ownerDocument?: Document;
|
|
139
|
-
}):
|
|
367
|
+
}): HTMLElement & {
|
|
368
|
+
value: string;
|
|
369
|
+
};
|
|
140
370
|
export declare function makeToggle(labelText: string, checked: boolean, onChange: (v: boolean) => void, ownerDocument?: Document): HTMLLabelElement;
|
|
141
371
|
export declare function makeLabeledSelect(labelText: string, options: readonly {
|
|
142
372
|
value: string;
|
|
@@ -173,4 +403,108 @@ export declare function attachChartTip(el: HTMLElement, card: HTMLElement, toolt
|
|
|
173
403
|
* window's own bounds).
|
|
174
404
|
*/
|
|
175
405
|
export declare function positionChartTooltip(tooltip: HTMLElement, card: HTMLElement, clientX: number, clientY: number): void;
|
|
406
|
+
/**
|
|
407
|
+
* The two axis toggles, shared across the distribution panels.
|
|
408
|
+
*
|
|
409
|
+
* They were per-panel state, so the same preference had to be set three times
|
|
410
|
+
* (histogram, boxplot, trend) for one test. The panels now exchange this through
|
|
411
|
+
* `insightsTab`, the same way they already share the selected test.
|
|
412
|
+
*
|
|
413
|
+
* `includeLimits: undefined` means "derive from the data" — see
|
|
414
|
+
* `shouldIncludeLimitsByDefault`. A boolean is an explicit user choice and sticks.
|
|
415
|
+
*/
|
|
416
|
+
export interface AxisPrefs {
|
|
417
|
+
includeLimits?: boolean;
|
|
418
|
+
clipOutliers: boolean;
|
|
419
|
+
}
|
|
420
|
+
/** Robust outlier fence over a value list: Tukey's `Q1 − k·IQR … Q3 + k·IQR`.
|
|
421
|
+
*
|
|
422
|
+
* Deliberately NOT mean ± 3σ. σ is computed FROM the data including the
|
|
423
|
+
* outlier, so a single reading of 1e30 inflates σ far enough that the fence no
|
|
424
|
+
* longer excludes it — the classic masking failure, and it fails hardest exactly
|
|
425
|
+
* when the outlier is worst. Quartiles are unmoved by the extreme tail.
|
|
426
|
+
*
|
|
427
|
+
* Returns null when there are too few values for quartiles to mean anything. */
|
|
428
|
+
export declare function robustFence(values: number[], k?: number): {
|
|
429
|
+
lo: number;
|
|
430
|
+
hi: number;
|
|
431
|
+
} | null;
|
|
432
|
+
/**
|
|
433
|
+
* Whether a panel should include the spec limits in its axis BY DEFAULT.
|
|
434
|
+
*
|
|
435
|
+
* Neither fixed answer is right. Always-off hides how close a distribution runs
|
|
436
|
+
* to its limit, which is the main thing a spec'd test is read for. Always-on
|
|
437
|
+
* squashes the data into a sliver whenever the limits are generous — and generous
|
|
438
|
+
* limits are what a capable process looks like (Ppk 2.0 means the data occupies
|
|
439
|
+
* about a third of the window), so the "good" case would render worst.
|
|
440
|
+
*
|
|
441
|
+
* So it is derived from the data, as this library derives other defaults: include
|
|
442
|
+
* the limits when doing so leaves the data at least `minDataShare` of the axis.
|
|
443
|
+
*/
|
|
444
|
+
export declare function shouldIncludeLimitsByDefault(dataMin: number, dataMax: number, limitLow: number | undefined, limitHigh: number | undefined, minDataShare?: number): boolean;
|
|
445
|
+
export interface AxisRange {
|
|
446
|
+
lo: number;
|
|
447
|
+
hi: number;
|
|
448
|
+
/** Limits that fall OUTSIDE [lo, hi] and so cannot be drawn in place — the
|
|
449
|
+
* caller should mark them at the axis edge instead. Without this a limit that
|
|
450
|
+
* is merely off-screen is indistinguishable from a test having no limit. */
|
|
451
|
+
offAxis: Array<{
|
|
452
|
+
value: number;
|
|
453
|
+
label: 'LSL' | 'USL';
|
|
454
|
+
side: 'lo' | 'hi';
|
|
455
|
+
}>;
|
|
456
|
+
/** Values excluded by the robust fence, when clipping is on. */
|
|
457
|
+
clippedCount: number;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Resolve a value axis from the data, the limits, and the two user toggles.
|
|
461
|
+
*
|
|
462
|
+
* `clipOutliers` clips the AXIS only. No caller may use it to drop values from a
|
|
463
|
+
* statistic: an out-of-spec die is a distribution outlier by construction, so
|
|
464
|
+
* excluding it would delete real spec failures from yield, and capability exists
|
|
465
|
+
* precisely to describe the tails. The count is returned so the panel can say how
|
|
466
|
+
* many points sit outside the view.
|
|
467
|
+
*/
|
|
468
|
+
export declare function resolveAxisRange(opts: {
|
|
469
|
+
dataMin: number;
|
|
470
|
+
dataMax: number;
|
|
471
|
+
limitLow?: number;
|
|
472
|
+
limitHigh?: number;
|
|
473
|
+
includeLimits: boolean;
|
|
474
|
+
clipOutliers?: boolean;
|
|
475
|
+
/** Raw values, needed only when `clipOutliers` is set. */
|
|
476
|
+
values?: number[];
|
|
477
|
+
}): AxisRange;
|
|
478
|
+
/**
|
|
479
|
+
* Draw an edge marker for each limit that falls outside the plotted range.
|
|
480
|
+
*
|
|
481
|
+
* Without this a limit that is merely off-screen renders as nothing at all, so a
|
|
482
|
+
* test whose limits sit beyond the axis is indistinguishable from a test with no
|
|
483
|
+
* limits — the reader cannot tell "comfortably inside spec" from "unspecced".
|
|
484
|
+
*
|
|
485
|
+
* `axis` is the plot rectangle in CSS px; `orient` says which way the value axis
|
|
486
|
+
* runs, so the same helper serves the horizontal-value panels (histogram,
|
|
487
|
+
* boxplot) and the vertical-value one (trend).
|
|
488
|
+
*/
|
|
489
|
+
/**
|
|
490
|
+
* Which side of its own rule a spec-limit label should sit on.
|
|
491
|
+
*
|
|
492
|
+
* Semantics first: LSL bounds the LOW out-of-spec region so its label belongs
|
|
493
|
+
* to the left of the line, USL to the right. A label on the far side sits in
|
|
494
|
+
* the in-spec region and reads as belonging to the data rather than to the
|
|
495
|
+
* boundary — which is how it looked when this was placed inward to keep it off
|
|
496
|
+
* the plot edge, a layout worry overriding what the mark means.
|
|
497
|
+
*
|
|
498
|
+
* The edge worry is real though, so it is handled rather than designed around:
|
|
499
|
+
* the semantically correct side wins unless the label would not fit there, and
|
|
500
|
+
* only then does it flip. Returns -1 to draw right-aligned left of the line,
|
|
501
|
+
* +1 to draw left-aligned right of it.
|
|
502
|
+
*/
|
|
503
|
+
export declare function limitLabelSide(x: number, textWidth: number, plotLeft: number, plotRight: number, isLowLimit: boolean, pad?: number): -1 | 1;
|
|
504
|
+
export declare function drawOffAxisLimits(ctx: CanvasRenderingContext2D, offAxis: AxisRange['offAxis'], axis: {
|
|
505
|
+
left: number;
|
|
506
|
+
right: number;
|
|
507
|
+
top: number;
|
|
508
|
+
bottom: number;
|
|
509
|
+
}, orient: 'horizontal' | 'vertical', color: string, format: (v: number) => string): void;
|
|
176
510
|
//# sourceMappingURL=chartShell.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CLR as a,Z_BASE as H,menuLayerFor as N,MENU_SEARCH_THRESHOLD as M,makeMenuSearchBox as I,markMenuTrigger as k,saveImageBlob as z,openReparentedModal as W}from"../toolbar.js";import{ICONS as O}from"../icons.js";import{fmt as F,fmtColorbarAxis as P}from"../../renderer/fmt.js";export function openChartExpandModal(e,r,n){const t=e.getAttribute("style")??"";if(Object.assign(e.style,{flex:"1",minHeight:"0",border:"none",borderRadius:"0"}),!W([e],{title:r,onClosed:()=>{e.setAttribute("style",t),n&&(n.style.display="flex")}})){e.setAttribute("style",t);return}n&&(n.style.display="none")}const _={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040"},j={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border"};export function resolveChartCanvasColors(e){const r=getComputedStyle(e),n={};for(const t of Object.keys(j)){const o=r.getPropertyValue(`--wmap-${j[t]}`).trim();n[t]=o||_[t]}return n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,r){const n=e.ownerDocument.defaultView??window;let t=0;const o=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,r()}))});return o.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,o.disconnect()}}}export function growCardToFitContent(e,r,n){const o=`${e.offsetHeight-r.clientHeight+n}px`;e.style.minHeight!==o&&(e.style.minHeight=o)}export function chartFillHeight(e,r,n,t){let o=0;for(const c of Array.from(r.children))c!==n&&(o+=c.offsetHeight);return growCardToFitContent(e,r,o+t),Math.max(t,r.clientHeight-o)}export function applyCanvasFlow(e,r=0){e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${r}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:F(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,r){if(!r)return{tick:formatValue,unitLabel:""};const{tickFmt:n,axisLabel:t}=P(e,null,r);return{tick:n,unitLabel:t}}export function drawAxisUnit(e,r,n,t,o){const c={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font="10px system-ui, sans-serif",e.textAlign="left",e.textBaseline="middle",e.fillStyle=o,e.fillText(`(${r})`,n,t),Object.assign(e,c)}export function saveCanvasPng(e,r,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const o=t.getContext("2d");o.fillStyle="#ffffff",o.fillRect(0,0,t.width,t.height),o.drawImage(e,0,0),t.toBlob(c=>{c&&z(c,r,n)},"image/png")}export function cardShell(e,r,n=document){const t=n.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"6px",padding:"12px",minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const o=t.ownerDocument.createElement("div");Object.assign(o.style,{display:"flex",alignItems:"center",gap:"6px",marginBottom:"6px"});const c=t.ownerDocument.createElement("div");c.textContent=e,Object.assign(c.style,{color:a.value,fontSize:"13px",fontWeight:"600",flex:"1"}),o.appendChild(c);const p=makeTooltip(t),i=t.ownerDocument.createElement("button");i.type="button",i.setAttribute("aria-label","Save as PNG"),i.innerHTML=O.download,Object.assign(i.style,{border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:"none",color:a.label,cursor:"pointer",width:"22px",height:"22px",lineHeight:"1",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),attachChartTip(i,t,p,"Save as PNG"),i.addEventListener("click",()=>{const l=t.querySelector("canvas");l&&saveCanvasPng(l,e,r)}),o.appendChild(i);const s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Expand"),s.innerHTML=O.expand,Object.assign(s.style,{border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:"none",color:a.label,cursor:"pointer",width:"22px",height:"22px",lineHeight:"1",flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),attachChartTip(s,t,p,"Expand"),s.addEventListener("click",()=>openChartExpandModal(t,c.textContent??e,s)),o.appendChild(s),t.appendChild(o);const d=t.ownerDocument.createElement("div");Object.assign(d.style,{display:"flex",gap:"6px",marginBottom:"6px",flexWrap:"wrap",alignItems:"center"}),t.appendChild(d);const u=t.ownerDocument.createElement("div");return Object.assign(u.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(u),{card:t,heading:c,controlsRow:d,body:u}}export function makeSegmented(e,r,n,t=document){const o=t.createElement("div");o.setAttribute("role","radiogroup"),Object.assign(o.style,{display:"inline-flex",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",overflow:"hidden"});const c=`seg-${Math.random().toString(36).slice(2,9)}`,p=[];return e.forEach(([i,s],d)=>{const u=t.createElement("label");Object.assign(u.style,{display:"inline-flex",alignItems:"center",fontSize:"12px",padding:"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:d>0?`1px solid ${a.menuBorder}`:"none"});const l=t.createElement("input");l.type="radio",l.name=c,l.value=i,l.checked=i===r,Object.assign(l.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const h=()=>{u.style.background=l.checked?a.bgActive:a.menuBg,u.style.color=l.checked?a.iconActive:a.text};p.push(h),h(),l.addEventListener("change",()=>{if(l.checked){for(const y of p)y();n(i)}}),u.append(l,t.createTextNode(s)),o.appendChild(u)}),o}export function makeBackButton(e,r=document){const n=r.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{fontSize:"12px",padding:"3px 10px",border:`1px solid ${a.menuBorder}`,borderRadius:"4px",background:a.menuBg,color:a.label,cursor:"pointer"}),n.addEventListener("click",e),n}export function renderEmptyState(e,r,n){const t=e.ownerDocument.createElement("div");t.textContent=r,Object.assign(t.style,{color:a.label,fontSize:"12px",padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeTestSelect(e,r,n,t={}){const{maxWidth:o="200px",emptyText:c="No parametric tests",ownerDocument:p=document}=t;if(e.length>M)return G(e,r,n,o,p);const i=p.createElement("select");if(Object.assign(i.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:o}),e.length===0){i.disabled=!0;const s=p.createElement("option");s.textContent=c,i.appendChild(s)}else{for(const s of e){const d=p.createElement("option");d.value=String(s.testNumber),d.textContent=s.name||`Test ${s.testNumber}`,s.testNumber===r&&(d.selected=!0),i.appendChild(d)}i.addEventListener("change",()=>n(Number(i.value)))}return i}function G(e,r,n,t,o=document){const c=x=>e.find(v=>v.testNumber===x)?.name||`Test ${x}`;let p=r;const i=o.createElement("button");i.type="button",Object.assign(i.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:t,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:"6px",justifyContent:"space-between"}),k(i,!1);const s=i.ownerDocument.createElement("span");Object.assign(s.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const d=i.ownerDocument.createElement("span");d.textContent="\u25BE",d.style.flex="0 0 auto",i.append(s,d);const u=()=>{s.textContent=p!==null?c(p):"(none)"};u();let l=null;const h=()=>{l&&(l.remove(),l=null,k(i,!1))},y=()=>{h(),i.focus()};function D(){const x=i.getBoundingClientRect(),v=i.ownerDocument.defaultView??window,S=Math.max(x.width,220),R=320,T=Math.min(x.left,Math.max(4,(v.innerWidth??1/0)-S-4));l=i.ownerDocument.createElement("div"),Object.assign(l.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${T}px`,background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",boxShadow:"0 4px 12px rgba(0,0,0,0.15)",zIndex:H,minWidth:`${S}px`,maxHeight:`${R}px`,overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const w=[],L=()=>w.filter(f=>f.row.style.display!=="none").map(f=>f.row),E=I(f=>{for(const m of w)m.row.style.display=m.label.includes(f)?"":"none"},"Filter tests\u2026",i.ownerDocument);Object.assign(E.style,{position:"sticky",top:"0",zIndex:"1",background:a.menuBg}),E.addEventListener("keydown",f=>{if(f.key==="Escape"){y();return}f.key==="ArrowDown"&&(f.preventDefault(),L()[0]?.focus())}),l.appendChild(E);for(const f of e){const m=f.name||`Test ${f.testNumber}`,b=f.testNumber===p,g=i.ownerDocument.createElement("div");g.textContent=m,g.setAttribute("role","menuitemradio"),g.setAttribute("aria-checked",b?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:"6px 14px",fontSize:"12px",cursor:"pointer",color:b?a.iconActive:a.text,fontWeight:b?"700":"400",background:b?a.menuActive:"transparent",whiteSpace:"nowrap",outline:"none"});const A=()=>{b||(g.style.background=a.menuHover)},B=()=>{g.style.background=b?a.menuActive:"transparent"};g.addEventListener("mouseenter",A),g.addEventListener("mouseleave",B),g.addEventListener("focus",A),g.addEventListener("blur",B),g.addEventListener("click",$=>{$.stopPropagation(),p=f.testNumber,u(),y(),n(f.testNumber)}),l.appendChild(g),w.push({row:g,label:m.toLowerCase()})}l.addEventListener("keydown",f=>{const m=L();if(m.length===0)return;const b=m.indexOf(i.ownerDocument.activeElement);switch(f.key){case"ArrowDown":f.preventDefault(),m[b<0||b===m.length-1?0:b+1].focus();break;case"ArrowUp":f.preventDefault(),m[b<=0?m.length-1:b-1].focus();break;case"Home":f.preventDefault(),m[0].focus();break;case"End":f.preventDefault(),m[m.length-1].focus();break;case"Enter":case" ":b>=0&&(f.preventDefault(),m[b].click());break;case"Escape":case"Tab":f.preventDefault(),y();break}}),l.setAttribute("role","menu"),l.setAttribute("aria-label","Test"),N(i).appendChild(l),k(i,!0)}i.addEventListener("click",x=>{if(x.stopPropagation(),l){h();return}D()});const C=x=>{if(!i.isConnected){i.ownerDocument.removeEventListener("click",C);return}l&&!l.contains(x.target)&&!i.contains(x.target)&&h()};return i.ownerDocument.addEventListener("click",C),Object.defineProperty(i,"value",{get:()=>p!==null?String(p):"",set:x=>{p=x===""?null:Number(x),u()}})}export function makeWaferSelect(e,r,n,t={}){const{maxWidth:o="160px",allLabel:c="All wafers",ownerDocument:p=document}=t,i="\0all",s=p.createElement("select");Object.assign(s.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:o});const d=p.createElement("option");return d.value=i,d.textContent=c,r===null&&(d.selected=!0),s.appendChild(d),e.forEach((u,l)=>{const h=p.createElement("option");h.value=String(l),h.textContent=u.label??`#${l}`,r===l&&(h.selected=!0),s.appendChild(h)}),s.addEventListener("change",()=>n(s.value===i?null:Number(s.value))),s}export function makeToggle(e,r,n,t=document){const o=t.createElement("label");Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:"4px",fontSize:"11px",color:a.label,cursor:"pointer",userSelect:"none"});const c=t.createElement("input");return c.type="checkbox",c.checked=r,c.style.cssText="margin:0;cursor:pointer;",c.addEventListener("change",()=>n(c.checked)),o.append(c,t.createTextNode(e)),o}export function makeLabeledSelect(e,r,n,t,o={}){const{maxWidth:c="160px",hook:p,ownerDocument:i=document}=o,s=i.createElement("label");s.textContent=e,Object.assign(s.style,{color:a.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const d=i.createElement("select");p&&(d.dataset.wmapSelect=p),Object.assign(d.style,{fontSize:"12px",padding:"2px 6px",background:a.menuBg,color:a.text,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",maxWidth:c});for(const u of r){const l=i.createElement("option");l.value=u.value,l.textContent=u.label,u.value===n&&(l.selected=!0),d.appendChild(l)}return d.addEventListener("change",()=>t(d.value)),s.appendChild(d),s}export function makeChartGridWrap(e=document){const r=e.createElement("div");return r.dataset.wmapChartGrid="1",Object.assign(r.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:"10px",flex:"0 0 auto"}),r}export function makeTooltip(e){const r=e.ownerDocument.createElement("div");return Object.assign(r.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:a.menuBg,border:`1px solid ${a.menuBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px",fontFamily:"system-ui, sans-serif",color:a.text,whiteSpace:"nowrap",boxShadow:"0 2px 8px rgba(0,0,0,0.3)"}),e.appendChild(r),r}export function attachChartTip(e,r,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",o=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,r,o.clientX,o.clientY)}),e.addEventListener("mousemove",o=>{n.style.display==="block"&&positionChartTooltip(n,r,o.clientX,o.clientY)}),e.addEventListener("mouseleave",()=>{n.style.display="none"}),e.addEventListener("focus",()=>{n.textContent=t,n.style.display="block";const o=e.getBoundingClientRect();positionChartTooltip(n,r,o.left+o.width/2,o.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,r,n,t){const o=r.getBoundingClientRect(),c=14,p=4;e.style.left="0",e.style.top="0";const i=e.offsetWidth,s=e.offsetHeight;let d=n-o.left+c,u=t-o.top+c;d+i+p>o.width&&(d=n-o.left-c-i),u+s+p>o.height&&(u=t-o.top-c-s),d=Math.max(p,d),u=Math.max(p,u),e.style.left=`${d}px`,e.style.top=`${u}px`}
|
|
1
|
+
import{SHADOW as I,LEADING as F,wireControlHover as L,controlStyle as T,SPACE as m,RADIUS as k,fontPx as j,FONT as v,CLR as f,Z_BASE as q,menuLayerFor as G,wireListNavigation as U,MENU_SEARCH_THRESHOLD as D,makeMenuSearchBox as Y,markMenuTrigger as R,saveImageBlob as X,openReparentedModal as Z}from"../toolbar.js";import{ICONS as N}from"../icons.js";import{fmt as K,fmtColorbarAxis as J}from"../../renderer/fmt.js";export function openChartExpandModal(e,r,n){for(const i of e.querySelectorAll('div[style*="position: absolute"]'))i.style.display==="block"&&i.style.pointerEvents==="none"&&(i.style.display="none");const t=e.getAttribute("style")??"";if(Object.assign(e.style,{flex:"1",minHeight:"0",border:"none",borderRadius:"0"}),!Z([e],{title:r,onClosed:()=>{e.setAttribute("style",t),n&&(n.style.display="flex")}})){e.setAttribute("style",t);return}n&&(n.style.display="none")}const Q={text:"#333",textMuted:"#66788a",border:"rgba(0,0,0,0.12)",bg:"#fff",bgHover:"#edf0f8",track:"#fafbfc",warnBorder:"#f0c040",accent:"#1a66cc",limitLine:"#b45309"},P={text:"text",textMuted:"text-muted",border:"border",bg:"surface",bgHover:"bg-hover",track:"panel-bg",warnBorder:"warn-border",accent:"icon-active",limitLine:"warn-text"};export function resolveChartCanvasColors(e){const r=getComputedStyle(e),n={};for(const t of Object.keys(P)){const o=r.getPropertyValue(`--wmap-${P[t]}`).trim();n[t]=o||Q[t]}return n}export const PADDING=12,VALUE_WIDTH=100;export function observeResize(e,r){const n=e.ownerDocument.defaultView??window;let t=0;const o=new n.ResizeObserver(()=>{t||(t=n.requestAnimationFrame(()=>{t=0,r()}))});return o.observe(e),{disconnect:()=>{t&&n.cancelAnimationFrame(t),t=0,o.disconnect()}}}export function growCardToFitContent(e,r,n){const o=`${e.offsetHeight-r.clientHeight+n}px`;e.style.minHeight!==o&&(e.style.minHeight=o)}export function chartFillHeight(e,r,n,t){const o=e.ownerDocument.defaultView??window;let i=0;for(const a of Array.from(r.children)){if(a===n)continue;const s=a,l=o.getComputedStyle(s);i+=s.offsetHeight+(parseFloat(l.marginTop)||0)+(parseFloat(l.marginBottom)||0)}return growCardToFitContent(e,r,i+t),Math.max(t,r.clientHeight-i)}export function chartDpr(e){return(e.ownerDocument.defaultView??window).devicePixelRatio||1}export function prepareCanvas(e,r,n,t){const o=chartDpr(e);e.width=Math.max(1,Math.floor(n*o)),e.height=Math.max(1,Math.floor(t*o)),e.style.width=`${n}px`,e.style.height=`${t}px`;const i=e.getContext("2d");return i?(i.setTransform(o,0,0,o,0,0),i.clearRect(0,0,n,t),i.font=`${j(-1)}px system-ui, sans-serif`,i.textBaseline="middle",{ctx:i,theme:resolveChartCanvasColors(r)}):null}export function applyCanvasFlow(e,r=0){let n;if(typeof r=="number")n=r;else{const t=(r.ownerDocument.defaultView??window).getComputedStyle(r);n=r.offsetTop+r.offsetHeight+(parseFloat(t.marginBottom)||0)}e.style.position="absolute",e.style.left="0",e.style.right="0",e.style.top=`${n}px`,e.style.bottom="0"}export function formatValue(e){return Number.isFinite(e)?Number.isInteger(e)?`${e}`:K(e,void 0,"engineering"):"\u2014"}export function makeAxisFormat(e,r){if(!r)return{tick:formatValue,unitLabel:""};const{tickFmt:n,axisLabel:t}=J(e,null,r);return{tick:n,unitLabel:t}}export function drawAxisUnit(e,r,n,t,o){const i={textAlign:e.textAlign,textBaseline:e.textBaseline,fillStyle:e.fillStyle,font:e.font};e.font=`${j(-1)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="middle",e.fillStyle=o,e.fillText(`(${r})`,n,t),Object.assign(e,i)}export function saveCanvasPng(e,r,n){const t=e.ownerDocument.createElement("canvas");t.width=e.width,t.height=e.height;const o=t.getContext("2d");o.fillStyle="#ffffff",o.fillRect(0,0,t.width,t.height),o.drawImage(e,0,0),t.toBlob(i=>{i&&X(i,r,n)},"image/png")}const z="10px";export function chartSwatchCss(e,r="solid"){return["display:inline-block",`width:${z}`,`height:${z}`,`border-radius:${k.pill}`,`background:${r==="outline"?"transparent":e}`,`border:1px ${r==="outline"?"dashed":"solid"} ${e}`,"box-sizing:border-box","flex-shrink:0"].join(";")}const ee="0.35";export function makeSeriesLegendItem(e,r,n=document){const t=n.createElement("button");t.type="button",Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:m.xs,padding:`${m.xs} ${m.md}`,borderRadius:k.pill,border:`1px solid ${f.menuBorder}`,background:"none",cursor:"pointer",fontSize:v.body,color:f.text,whiteSpace:"nowrap"}),L(t,"bare");const o=n.createElement("span");return o.style.cssText=chartSwatchCss(r),t.append(o,n.createTextNode(e)),{el:t,setState({selected:i,dimmed:a}){t.style.borderColor=i?f.iconActive:f.menuBorder,t.style.fontWeight=i?"600":"400",t.style.opacity=a?ee:"1",t.setAttribute("aria-pressed",i?"true":"false")}}}export function cardFrameStyle(){return{background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:k.container,padding:m.xl,fontVariantNumeric:"tabular-nums"}}export function cardShell(e,r,n=document){const t=n.createElement("div");t.dataset.wmapChartCard="1",t.dataset.wmapChartTitle=e,Object.assign(t.style,{display:"flex",flexDirection:"column",...cardFrameStyle(),minWidth:"0",minHeight:"0",flex:"1 1 0",position:"relative"});const o=t.ownerDocument.createElement("div");Object.assign(o.style,{display:"flex",alignItems:"center",gap:m.sm,paddingBottom:"6px",marginBottom:m.md,borderBottom:`1px solid ${f.menuBorder}`});const i=t.ownerDocument.createElement("div");i.textContent=e,Object.assign(i.style,{color:f.value,fontSize:v.heading,fontWeight:"600",flex:"1"}),o.appendChild(i);const a=makeTooltip(t),s=t.ownerDocument.createElement("button");s.type="button",s.setAttribute("aria-label","Save as PNG"),s.innerHTML=N.download,Object.assign(s.style,{...T("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:F.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(s),attachChartTip(s,t,a,"Save as PNG"),s.addEventListener("click",()=>{const p=t.querySelector("canvas");p&&saveCanvasPng(p,e,r)}),o.appendChild(s);const l=t.ownerDocument.createElement("button");l.type="button",l.setAttribute("aria-label","Expand"),l.innerHTML=N.expand,Object.assign(l.style,{...T("outlined"),color:f.label,padding:"0",width:"22px",height:"22px",lineHeight:F.none,flexShrink:"0",display:"flex",alignItems:"center",justifyContent:"center"}),L(l),attachChartTip(l,t,a,"Expand"),l.addEventListener("click",()=>openChartExpandModal(t,i.textContent??e,l)),o.appendChild(l),t.appendChild(o);const c=t.ownerDocument.createElement("div");Object.assign(c.style,{display:"flex",gap:m.sm,marginBottom:m.md,flexWrap:"wrap",alignItems:"center"}),t.appendChild(c);const d=t.ownerDocument.createElement("div");return Object.assign(d.style,{overflowX:"hidden",overflowY:"hidden",minHeight:"0",flex:"1",position:"relative"}),t.appendChild(d),{card:t,heading:i,controlsRow:c,body:d}}export function makeSegmented(e,r,n,t=document,o=!1){const i=t.createElement("div");i.setAttribute("role","radiogroup"),Object.assign(i.style,{display:"inline-flex",border:`1px solid ${f.menuBorder}`,borderRadius:k.control,overflow:"hidden"});const a=`seg-${Math.random().toString(36).slice(2,9)}`,s=[];return e.forEach(([l,c],d)=>{const p=t.createElement("label");Object.assign(p.style,{display:"inline-flex",alignItems:"center",fontSize:v.body,padding:o?"1px 6px":"3px 10px",cursor:"pointer",userSelect:"none",borderLeft:d>0?`1px solid ${f.menuBorder}`:"none"}),L(p,"bare");const u=t.createElement("input");u.type="radio",u.name=a,u.value=l,u.checked=l===r,Object.assign(u.style,{position:"absolute",opacity:"0",width:"0",height:"0"});const w=()=>{p.style.background=u.checked?f.bgActive:f.menuBg,p.style.color=u.checked?f.iconActive:f.text,p.dataset.on=u.checked?"true":"false"};s.push(w),w(),u.addEventListener("change",()=>{if(u.checked){for(const C of s)C();n(l)}}),p.append(u,t.createTextNode(c)),i.appendChild(p)}),i}export function makeBackButton(e,r=document){const n=r.createElement("button");return n.type="button",n.textContent="\u2190 Back",Object.assign(n.style,{...T("outlined"),background:f.menuBg,color:f.label}),L(n),n.addEventListener("click",e),n}export function renderEmptyState(e,r,n){const t=e.ownerDocument.createElement("div");t.textContent=r,Object.assign(t.style,{color:f.label,fontSize:v.body,padding:"8px 0"}),n&&Object.assign(t.style,n),e.appendChild(t)}export function makeListSelect(e,r,n,t={}){const{maxWidth:o="200px",ownerDocument:i=document,ariaLabel:a,emptyText:s,searchPlaceholder:l="Filter\u2026",hook:c}=t;let d=r;const p=x=>e.find(O=>O.value===x)?.label??"",u=i.createElement("button");u.type="button",c&&(u.dataset.wmapSelect=c),a&&u.setAttribute("aria-label",a),Object.assign(u.style,{fontSize:v.body,padding:`${m.xxs} ${m.sm}`,background:f.menuBg,color:f.text,border:`1px solid ${f.menuBorder}`,borderRadius:k.control,maxWidth:o,textAlign:"left",cursor:"pointer",display:"inline-flex",alignItems:"center",gap:m.sm,justifyContent:"space-between"}),L(u),R(u,!1);const w=i.createElement("span");Object.assign(w.style,{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"});const C=i.createElement("span");C.textContent="\u25BE",C.style.flex="0 0 auto",u.append(w,C);const S=()=>{w.textContent=e.length===0?s??"(none)":p(d)||s||"(none)"};if(S(),e.length===0)return u.disabled=!0,u.style.opacity="0.6",u.style.cursor="default",Object.defineProperty(u,"value",{get:()=>d,set:x=>{d=x,S()}});let h=null;const E=()=>{h&&(h.remove(),h=null,R(u,!1))},A=()=>{E(),u.focus()};function W(){const x=u.getBoundingClientRect(),O=i.defaultView??window,H=Math.max(x.width,220),_=Math.min(x.left,Math.max(4,(O.innerWidth??1/0)-H-4));h=i.createElement("div"),h.setAttribute("role","listbox"),h.setAttribute("aria-label",a??"Options"),Object.assign(h.style,{position:"fixed",top:`${x.bottom+4}px`,left:`${_}px`,background:f.menuBg,border:`1px solid ${f.menuBorder}`,borderRadius:k.control,boxShadow:I.menu,zIndex:q,minWidth:`${H}px`,maxHeight:"320px",overflowY:"auto",padding:"4px 0",pointerEvents:"auto"});const $=[],B=()=>$.filter(b=>b.row.style.display!=="none").map(b=>b.row);if(e.length>D){const b=Y(y=>{for(const g of $)g.row.style.display=g.label.includes(y)?"":"none"},l,i);Object.assign(b.style,{position:"sticky",top:"0",zIndex:"1",background:f.menuBg}),b.addEventListener("keydown",y=>{if(y.key==="Escape"){A();return}y.key==="ArrowDown"&&(y.preventDefault(),B()[0]?.focus())}),h.appendChild(b)}for(const b of e){const y=b.value===d,g=i.createElement("div");g.textContent=b.label,g.setAttribute("role","option"),g.setAttribute("aria-selected",y?"true":"false"),g.tabIndex=-1,Object.assign(g.style,{padding:`${m.sm} ${m.xl}`,fontSize:v.body,cursor:"pointer",color:y?f.iconActive:f.text,fontWeight:y?"700":"400",background:y?f.menuActive:"transparent",whiteSpace:"nowrap"}),g.addEventListener("mouseenter",()=>{y||(g.style.background=f.menuHover)}),g.addEventListener("mouseleave",()=>{y||(g.style.background="transparent")}),g.addEventListener("click",V=>{V.stopPropagation(),d=b.value,S(),A(),n(b.value)}),h.appendChild(g),$.push({row:g,label:b.label.toLowerCase()})}if(U(h,B,A),G(u).appendChild(h),R(u,!0),e.length<=D){const b=e.findIndex(y=>y.value===d);B()[b>=0?b:0]?.focus()}}u.addEventListener("click",x=>{if(x.stopPropagation(),h){E();return}W()});const M=x=>{if(!u.isConnected){i.removeEventListener("click",M);return}h&&!h.contains(x.target)&&!u.contains(x.target)&&E()};return i.addEventListener("click",M),Object.defineProperty(u,"value",{get:()=>d,set:x=>{d=x,S()}})}export function makeTestSelect(e,r,n,t={}){const{maxWidth:o="200px",emptyText:i="No parametric tests",ownerDocument:a=document}=t;return makeListSelect(e.map(s=>({value:String(s.testNumber),label:s.name||`Test ${s.testNumber}`})),r!==null?String(r):"",s=>n(Number(s)),{maxWidth:o,ownerDocument:a,ariaLabel:"Test",emptyText:i,searchPlaceholder:"Filter tests\u2026"})}export function makeLinkedAxisPrefs(e,r,n,t=document){let o=r?.includeLimits,i=r?.clipOutliers??!1;const a=()=>n({includeLimits:o,clipOutliers:i});return{get:()=>({includeLimits:o,clipOutliers:i}),sync(s,l){e.innerHTML="",l&&e.appendChild(makeToggle("Axis includes limits",s,c=>{o=c,a()},t)),e.appendChild(makeToggle("Clip outliers",i,c=>{i=c,a()},t))},set(s){return s.includeLimits===o&&s.clipOutliers===i?!1:(o=s.includeLimits,i=s.clipOutliers,!0)}}}export function makeLinkedTestSelect(e,r,n,t={}){let o=r;const i=makeTestSelect(e,o,a=>{a!==o&&(o=a,n(a))},t);return{el:i,get:()=>o,set(a){return o===a||!e.some(s=>s.testNumber===a)?!1:(o=a,i.value=String(a),!0)}}}export const ALL_GROUPS="\0all";export function makeLinkedGroupSelect(e,r,n,t,o={}){let i=n;const a=d=>d??ALL_GROUPS,s=d=>d===ALL_GROUPS?null:d,l=makeLabeledSelect(r,[{value:ALL_GROUPS,label:"All groups"},...e.map(d=>({value:d,label:d}))],a(i),d=>{const p=s(d);p!==i&&(i=p,t(p))},{...o,hook:"group-scope"}),c=l.querySelector('[data-wmap-select="group-scope"]');return{el:l,get:()=>i,set(d){return i===d||d!==null&&!e.includes(d)?!1:(i=d,c&&(c.value=a(d)),!0)}}}export function makeWaferSelect(e,r,n,t={}){const{maxWidth:o="160px",allLabel:i="All wafers",ownerDocument:a=document}=t,s="\0all";return makeListSelect([{value:s,label:i},...e.map((l,c)=>({value:String(c),label:l.label??`#${c}`}))],r===null?s:String(r),l=>n(l===s?null:Number(l)),{maxWidth:o,ownerDocument:a,ariaLabel:i,searchPlaceholder:"Filter wafers\u2026"})}export function makeToggle(e,r,n,t=document){const o=t.createElement("label");Object.assign(o.style,{display:"inline-flex",alignItems:"center",gap:m.xs,fontSize:v.body,color:f.label,cursor:"pointer",userSelect:"none"}),L(o,"bare");const i=t.createElement("input");return i.type="checkbox",i.checked=r,i.style.cssText="margin:0;cursor:pointer;",i.addEventListener("change",()=>n(i.checked)),o.append(i,t.createTextNode(e)),o}export function makeLabeledSelect(e,r,n,t,o={}){const{maxWidth:i="160px",hook:a,ownerDocument:s=document}=o,l=s.createElement("label");l.textContent=e,Object.assign(l.style,{color:f.label,fontSize:v.body,display:"flex",alignItems:"center",gap:m.xs});const c=makeListSelect(r,n,t,{maxWidth:i,ownerDocument:s,hook:a,ariaLabel:e.replace(/:\s*$/,"")});return l.appendChild(c),l}export function makeChartGridWrap(e=document){const r=e.createElement("div");return r.dataset.wmapChartGrid="1",Object.assign(r.style,{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(420px, 1fr))",gap:m.lg,flex:"0 0 auto"}),r}export function makeTooltip(e){const r=e.ownerDocument.createElement("div");return Object.assign(r.style,{position:"absolute",display:"none",pointerEvents:"none",zIndex:"50",background:"rgba(30, 32, 40, 0.93)",color:"#f0f0f2",border:"1px solid rgba(255,255,255,0.10)",borderRadius:k.control,padding:`${m.sm} ${m.xl}`,fontSize:v.sub,fontFamily:v.family,maxWidth:"280px",whiteSpace:"nowrap",boxShadow:I.menu}),e.appendChild(r),r}export function attachChartTip(e,r,n,t){e.removeAttribute("title"),e.textContent?.trim()||(e.ariaLabel=t),e.addEventListener("mouseenter",o=>{n.textContent=t,n.style.display="block",positionChartTooltip(n,r,o.clientX,o.clientY)}),e.addEventListener("mousemove",o=>{n.style.display==="block"&&positionChartTooltip(n,r,o.clientX,o.clientY)}),e.addEventListener("mouseleave",()=>{n.style.display="none"}),e.addEventListener("click",()=>{n.style.display="none"}),e.addEventListener("focus",()=>{n.textContent=t,n.style.display="block";const o=e.getBoundingClientRect();positionChartTooltip(n,r,o.left+o.width/2,o.bottom)}),e.addEventListener("blur",()=>{n.style.display="none"})}export function positionChartTooltip(e,r,n,t){const o=r.getBoundingClientRect(),i=14,a=4;e.style.left="0",e.style.top="0";const s=e.offsetWidth,l=e.offsetHeight;let c=n-o.left+i,d=t-o.top+i;c+s+a>o.width&&(c=n-o.left-i-s),d+l+a>o.height&&(d=t-o.top-i-l),c=Math.max(a,c),d=Math.max(a,d),e.style.left=`${c}px`,e.style.top=`${d}px`}export function robustFence(e,r=1.5){const n=e.filter(s=>Number.isFinite(s)).sort((s,l)=>s-l);if(n.length<8)return null;const t=s=>{const l=(n.length-1)*s,c=Math.floor(l),d=Math.ceil(l);return c===d?n[c]:n[c]+(n[d]-n[c])*(l-c)},o=t(.25),i=t(.75),a=i-o;return a===0?null:{lo:o-r*a,hi:i+r*a}}export function shouldIncludeLimitsByDefault(e,r,n,t,o=1/3){if(n===void 0&&t===void 0||!Number.isFinite(e)||!Number.isFinite(r))return!1;const i=Math.min(e,n??e),s=Math.max(r,t??r)-i;if(s<=0)return!0;const l=r-e;return l===0?!0:l/s>=o}export function resolveAxisRange(e){const{dataMin:r,dataMax:n,limitLow:t,limitHigh:o,includeLimits:i,clipOutliers:a,values:s}=e;let l=r,c=n,d=0;if(a&&s?.length){const u=robustFence(s);if(u){const w=Math.max(l,u.lo),C=Math.min(c,u.hi);C>w&&(d=s.filter(S=>Number.isFinite(S)&&(S<w||S>C)).length,l=w,c=C)}}i&&(t!==void 0&&(l=Math.min(l,t)),o!==void 0&&(c=Math.max(c,o))),l===c&&(l-=1,c+=1);const p=[];return t!==void 0&&t<l&&p.push({value:t,label:"LSL",side:"lo"}),t!==void 0&&t>c&&p.push({value:t,label:"LSL",side:"hi"}),o!==void 0&&o>c&&p.push({value:o,label:"USL",side:"hi"}),o!==void 0&&o<l&&p.push({value:o,label:"USL",side:"lo"}),{lo:l,hi:c,offAxis:p,clippedCount:d}}export function limitLabelSide(e,r,n,t,o,i=3){const a=o?-1:1;return(a<0?e-i-r>=n:e+i+r<=t)?a:-a}export function drawOffAxisLimits(e,r,n,t,o,i){if(r.length){e.save(),e.fillStyle=o,e.font=`${j(-1)}px system-ui, sans-serif`;for(const{value:a,label:s,side:l}of r){const c=t==="horizontal"?l==="lo"?"\u2190":"\u2192":l==="lo"?"\u2193":"\u2191",d=l==="lo"?`${c} ${s} ${i(a)}`:`${s} ${i(a)} ${c}`;t==="horizontal"?(e.textBaseline="top",e.textAlign=l==="lo"?"left":"right",e.fillText(d,l==="lo"?n.left+2:n.right-2,n.top+2)):(e.textBaseline=l==="lo"?"bottom":"top",e.textAlign="left",e.fillText(d,n.left+4,l==="lo"?n.bottom-2:n.top+2))}e.restore()}}
|
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
import { type FacetItem } from '../../stats/facets.js';
|
|
2
2
|
import type { Die } from '../../core/dies.js';
|
|
3
3
|
import type { TestDef } from '../../renderer/buildWaferMap.js';
|
|
4
|
+
import { type SaveTextHandler } from '../toolbar.js';
|
|
4
5
|
import { type SaveImageHandler } from './chartShell.js';
|
|
5
6
|
type CorrelationItem = FacetItem & {
|
|
6
7
|
dies?: Die[];
|
|
7
8
|
label?: string;
|
|
8
9
|
};
|
|
9
10
|
export interface CorrelationPanelOptions {
|
|
11
|
+
/** Host hook for the Export CSV button — see `saveTextFile` (toolbar.ts).
|
|
12
|
+
* Omit and no button is offered. */
|
|
13
|
+
onSaveText?: SaveTextHandler;
|
|
10
14
|
title?: string;
|
|
11
15
|
items: CorrelationItem[];
|
|
12
16
|
testDefs: TestDef[];
|
|
13
17
|
colorScheme?: string;
|
|
14
18
|
onSaveImage?: SaveImageHandler;
|
|
15
19
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* is provided. Absent ⇒ today's plain ungrouped behavior.
|
|
20
|
+
* Grouping is NOT this panel's concern — the Insights tab owns one "Show:"
|
|
21
|
+
* scope for every view and hands each panel the population it names. This
|
|
22
|
+
* previously took a `groups` list and silently restricted to `groups[0]`,
|
|
23
|
+
* with no way back to the pooled matrix.
|
|
21
24
|
*/
|
|
22
|
-
groups?: {
|
|
23
|
-
key: string;
|
|
24
|
-
items: CorrelationItem[];
|
|
25
|
-
}[];
|
|
26
25
|
/** Clicking a non-diagonal cell calls this — the Analysis tab wires it to drive the scatter panel's X/Y in place. */
|
|
27
26
|
onSelectPair?: (xTestNumber: number, yTestNumber: number) => void;
|
|
28
27
|
/** Document to build this panel's DOM into. Default `document` — pass the
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
import{buildCorrelationMatrix as
|
|
1
|
+
import{buildCorrelationMatrix as yt,filterCorrelationMatrix as Mt}from"../../stats/correlation.js";import{csvField as rt}from"../../core/utils.js";import{CORRELATION_POSITIVE as it,CORRELATION_NEGATIVE as at}from"./palette.js";import{buildFacetTable as wt}from"../../stats/facets.js";import{wireControlHover as vt,controlStyle as Lt,SPACE as C,RADIUS as tt,fontPx as ct,FONT as z,CLR as b,saveTextFile as Ct}from"../toolbar.js";import{attachChartTip as It,cardShell as Tt,observeResize as St,makeTooltip as Et,positionChartTooltip as $t,makeWaferSelect as Rt,renderEmptyState as Dt,resolveChartCanvasColors as kt,prepareCanvas as At,chartDpr as dt}from"./chartShell.js";const ut=5,ft=100,pt=20;function et(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 Bt(x){const d=et(x);return d?(d[0]*299+d[1]*587+d[2]*114)/1e3>140?"#111":"#fff":"#000"}function Ot(x,d,f){const B=et(x);if(!B)return x;const G=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(${G},${O},${l})`}export function renderCorrelationPanel(x){const{title:d="Test correlation matrix",items:f,testDefs:B,onSaveImage:G,onSelectPair:O}=x,{card:l,body:D,controlsRow:U}=Tt(d,G,x.ownerDocument);D.style.overflowX="auto",l.style.alignSelf="start";let Y=pt,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(ut),L.max=String(ft),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:tt.control}),L.addEventListener("change",()=>{const e=Math.max(ut,Math.min(ft,parseInt(L.value,10)||pt));L.value=String(e),e!==Y&&(Y=e,J())}),H.appendChild(L),U.appendChild(H),f.length>1&&U.appendChild(Rt(f,j,e=>{j=e,J()},{ownerDocument:l.ownerDocument}));function mt(){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,{...Lt("outlined"),color:b.text}),vt(e),e.addEventListener("click",()=>{const o=nt;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([rt(o.tests[p].label),String(o.tests[p].testNumber),rt(o.tests[r].label),String(o.tests[r].testNumber),i.r===null?"":i.r.toFixed(6),String(i.n)].join(","))}Ct(u.join(`
|
|
2
|
+
`),"test-correlation.csv","text/csv",x.onSaveText)}),U.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);It(H,l,I,"Cap on how many tests the matrix includes (strongest correlations kept first)");let q=()=>{};function ht(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:tt.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:tt.control,border:`1px solid ${b.menuBorder}`,background:`linear-gradient(to right, ${at}, ${b.menuBg}, ${it})`});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 xt(e){if(D.innerHTML="",e.tests.length<2)return Dt(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=kt(l),c=et(s.bg)??[255,255,255],h=D.clientWidth,n=w(h),W=n*u,S=g+n*u+4,E=i+W,$=At(o,l,E,S);if(!$)return;const{ctx:t}=$;t.font=`${ct(-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,K=i+y*n+n/2;t.save(),t.translate(K,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,K=A+n/2;t.font=`${ct(-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,K),(F.get(y)??[]).forEach(st=>{const Q=st.xIndex,P=i+Q*n,bt=Q===a&&y===M,lt=Q===y,V=st.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 Z=s.bgHover;lt?t.fillStyle=s.bgHover:(Z=Ot(V>=0?it:at,c,Math.abs(V)),t.fillStyle=Z),t.fillRect(P+1,A+1,n-2,n-2),n>=T&&!lt&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Bt(Z),t.textAlign="center",t.textBaseline="middle",t.fillText(V.toFixed(2),P+n/2,A+n/2)),bt&&(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 ot(s){const c=o.getBoundingClientRect(),h=D.clientWidth,n=w(h),W=(s.clientX-c.left)*(o.width/dt(o)/c.width),S=(s.clientY-c.top)*(o.height/dt(o)/c.height),E=Math.floor((W-i)/n),$=Math.floor((S-g)/n);return E>=0&&E<u&&$>=0&&$<u?{xi:E,yi:$}:null}return o.addEventListener("mousemove",s=>{const c=ot(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=e.tests[h].label,$=e.tests[n].label;W?I.innerHTML=`<strong>${E}</strong>`:S?.r!==null&&S?.r!==void 0?I.innerHTML=`<strong>${r(e.tests[n])}</strong> (#${e.tests[n].testNumber}) vs <strong>${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=`${$} vs ${E}<br><em>insufficient data</em>`,I.style.display="block",$t(I,l,s.clientX,s.clientY)}),o.addEventListener("mouseleave",()=>{I.style.display="none"}),o.addEventListener("click",s=>{const c=ot(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 nt=null;function J(){const e=mt(),o=e.flatMap(a=>a.dies??[]),u=yt(o,B),{matrix:r,strongPairs:p,moderatePairs:i,hiddenWeakPairs:m,strongestPair:g}=Mt(u,{minTests:6,maxTests:Y}),k=wt(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;nt=r,ht(p,i,m,g,k,N),q=xt(r),q()}const gt=St(l,()=>q());return J(),{card:l,destroy:()=>gt.disconnect()}}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** One cluster: a label, a sub-bar per group, and a trailing value. */
|
|
2
|
+
export interface GroupedBarRow {
|
|
3
|
+
label: string;
|
|
4
|
+
/**
|
|
5
|
+
* One entry per group, in group order.
|
|
6
|
+
*
|
|
7
|
+
* `fraction` is the bar's share of the track, 0-1 — already resolved by the
|
|
8
|
+
* caller, because that is exactly where the two charts differ (normalised to
|
|
9
|
+
* the chart maximum vs a fixed 0-100% scale). `null` means nothing was
|
|
10
|
+
* measured for this group, drawn as an empty track with `emptyBarText`
|
|
11
|
+
* rather than a zero-width bar, which would read as a real zero.
|
|
12
|
+
*/
|
|
13
|
+
bars: Array<{
|
|
14
|
+
fraction: number | null;
|
|
15
|
+
}>;
|
|
16
|
+
/** Right-hand value column, formatted by the caller. */
|
|
17
|
+
trailing: string;
|
|
18
|
+
}
|
|
19
|
+
export interface GroupedBarPlotOptions {
|
|
20
|
+
rows: GroupedBarRow[];
|
|
21
|
+
/** Group names, in the order `GroupedBarRow.bars` uses. Drives the legend. */
|
|
22
|
+
groups: string[];
|
|
23
|
+
/** Width reserved for the row labels. */
|
|
24
|
+
labelWidth: number;
|
|
25
|
+
/** Rows visible before the plot scrolls. */
|
|
26
|
+
maxVisibleRows?: number;
|
|
27
|
+
/** Show the group legend above the plot. */
|
|
28
|
+
showLegend?: boolean;
|
|
29
|
+
/** Truncate row labels beyond this many characters. Off when omitted. */
|
|
30
|
+
maxLabelChars?: number;
|
|
31
|
+
/** Drawn inside an empty track where `fraction` is null. */
|
|
32
|
+
emptyBarText?: string;
|
|
33
|
+
/** Sub-bar colour by group index. Defaults to the CVD-safe categorical ramp. */
|
|
34
|
+
colorOf?: (groupIndex: number) => string;
|
|
35
|
+
/** Tooltip HTML for a hovered sub-bar. */
|
|
36
|
+
tooltipHtml: (rowIndex: number, groupIndex: number) => string;
|
|
37
|
+
}
|
|
38
|
+
export interface GroupedBarPlotHandle {
|
|
39
|
+
/** Redraw — call when the theme changes or data is replaced. */
|
|
40
|
+
draw(): void;
|
|
41
|
+
destroy(): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build the legend, scroll area and canvas into `body`, and wire hover.
|
|
45
|
+
*
|
|
46
|
+
* `card` is needed for the theme and for tooltip positioning; `tooltip` is the
|
|
47
|
+
* caller's own element so a panel keeps one tooltip across rebuilds.
|
|
48
|
+
*/
|
|
49
|
+
export declare function renderGroupedBarPlot(card: HTMLElement, body: HTMLElement, tooltip: HTMLElement, options: GroupedBarPlotOptions): GroupedBarPlotHandle;
|
|
50
|
+
//# sourceMappingURL=groupedBarPlot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{categorical as Y}from"./palette.js";import{SPACE as E,FONT as q,CLR as J}from"../toolbar.js";import{observeResize as K,positionChartTooltip as Q,growCardToFitContent as Z,prepareCanvas as tt,chartSwatchCss as et,PADDING as c,VALUE_WIDTH as D}from"./chartShell.js";const nt=8,w=1,a=14,lt=8;export function renderGroupedBarPlot(h,C,d,U){const{rows:y,groups:H,labelWidth:L,tooltipHtml:$,maxVisibleRows:j=lt,showLegend:k=!0,maxLabelChars:A,emptyBarText:R,colorOf:B=Y}=U,f=h.ownerDocument,M=Math.max(1,H.length),S=M*a+(M-1)*w,u=S+nt;let W=0;if(k){const e=f.createElement("div");Object.assign(e.style,{display:"flex",flexWrap:"wrap",gap:`${E.xs} ${E.xl}`,marginBottom:E.xs}),H.forEach((o,n)=>{const t=f.createElement("span");Object.assign(t.style,{display:"inline-flex",alignItems:"center",gap:E.sm,fontSize:q.body,color:J.text});const l=f.createElement("span");l.style.cssText=et(B(n));const s=f.createElement("span");s.textContent=o,t.append(l,s),e.appendChild(t)}),C.appendChild(e),W=e.offsetHeight}const v=f.createElement("div"),_=c*2+Math.min(y.length,j)*u;Object.assign(v.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${_}px`,scrollbarGutter:"stable"}),Z(h,C,W+_),C.appendChild(v);const i=f.createElement("canvas");i.style.display="block",i.style.cursor="default",v.appendChild(i);let r=null;function G(){const e=c+L,o=Math.max(10,i.clientWidth-e-D-c);return{barX:e,barMaxWidth:o}}function z(e,o){const n=Math.floor((o-c)/u);if(n<0||n>=y.length)return null;const t=o-c-n*u,l=Math.floor(t/(a+w));if(l<0||l>=M)return null;const{barX:s,barMaxWidth:g}=G();return e<s||e>s+g?null:{row:n,group:l}}function p(){const e=v.clientWidth,o=c*2+y.length*u,n=tt(i,h,e,o);if(!n)return;const{ctx:t,theme:l}=n,{barX:s,barMaxWidth:g}=G();y.forEach((m,P)=>{const b=c+P*u;if(r&&r.row===P){const x=b+r.group*(a+w);t.fillStyle=l.bgHover,t.fillRect(0,x-1,e,a+2)}t.fillStyle=l.text,t.textAlign="right";const V=A!==void 0&&m.label.length>A?`${m.label.slice(0,A-1)}\u2026`:m.label;t.fillText(V,c+L-8,b+S/2),m.bars.forEach((x,X)=>{const T=b+X*(a+w);if(t.fillStyle=l.track,t.fillRect(s,T,g,a),x.fraction===null){R&&(t.fillStyle=l.textMuted,t.textAlign="left",t.fillText(R,s+4,T+a/2));return}const N=Math.max(x.fraction>0?1:0,x.fraction*g);t.fillStyle=B(X),t.fillRect(s,T,N,a)}),t.fillStyle=l.text,t.textAlign="right",t.fillText(m.trailing,s+g+D,b+S/2)})}const I=e=>{const o=i.getBoundingClientRect(),n=z(e.clientX-o.left,e.clientY-o.top),t=n?.row!==r?.row||n?.group!==r?.group;if(r=n,t&&p(),!n){d.style.display="none";return}d.innerHTML=$(n.row,n.group),d.style.display="block",Q(d,h,e.clientX,e.clientY)},O=()=>{r&&(r=null,p()),d.style.display="none"};i.addEventListener("mousemove",I),i.addEventListener("mouseleave",O);const F=K(h,()=>p());return p(),{draw:p,destroy:()=>{F?.disconnect(),i.removeEventListener("mousemove",I),i.removeEventListener("mouseleave",O)}}}
|