@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.
Files changed (88) hide show
  1. package/CHANGELOG.md +360 -0
  2. package/README.md +46 -3
  3. package/dist/packages/canvas-adapter/canvasTheme.d.ts +4 -0
  4. package/dist/packages/canvas-adapter/canvasTheme.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +16 -0
  6. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +13 -1
  9. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/capability.d.ts +10 -10
  11. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +337 -3
  13. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/correlation.d.ts +8 -9
  15. package/dist/packages/canvas-adapter/charts/correlation.js +2 -1
  16. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +50 -0
  17. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -0
  18. package/dist/packages/canvas-adapter/charts/histogram.d.ts +12 -1
  19. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  20. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  21. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  22. package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +20 -0
  23. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -0
  24. package/dist/packages/canvas-adapter/charts/trend.d.ts +30 -0
  25. package/dist/packages/canvas-adapter/charts/trend.js +1 -0
  26. package/dist/packages/canvas-adapter/dieList.js +7 -7
  27. package/dist/packages/canvas-adapter/identityHeader.d.ts +46 -0
  28. package/dist/packages/canvas-adapter/identityHeader.js +1 -0
  29. package/dist/packages/canvas-adapter/index.d.ts +1 -1
  30. package/dist/packages/canvas-adapter/insightsTab.d.ts +27 -3
  31. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  32. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  33. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +15 -2
  34. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -20
  35. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +72 -17
  36. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  37. package/dist/packages/canvas-adapter/summaryPanel.d.ts +219 -33
  38. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  39. package/dist/packages/canvas-adapter/toCanvas.d.ts +6 -0
  40. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  41. package/dist/packages/canvas-adapter/toolbar.d.ts +310 -6
  42. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  43. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  44. package/dist/packages/canvas-adapter/userGuideHtml.js +97 -34
  45. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  46. package/dist/packages/canvas-adapter/version.js +1 -1
  47. package/dist/packages/canvas-adapter/warnings.d.ts +0 -4
  48. package/dist/packages/canvas-adapter/warnings.js +1 -1
  49. package/dist/packages/core/utils.d.ts +11 -0
  50. package/dist/packages/core/utils.js +1 -1
  51. package/dist/packages/renderer/buildView.d.ts +34 -1
  52. package/dist/packages/renderer/buildView.js +1 -1
  53. package/dist/packages/renderer/buildWaferMap.d.ts +104 -5
  54. package/dist/packages/renderer/buildWaferMap.js +1 -1
  55. package/dist/packages/renderer/colorSchemes.js +1 -1
  56. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  57. package/dist/packages/stats/binPareto.d.ts +15 -0
  58. package/dist/packages/stats/binPareto.js +1 -1
  59. package/dist/packages/stats/clusterDetection.js +1 -1
  60. package/dist/packages/stats/connectedComponents.d.ts +14 -0
  61. package/dist/packages/stats/connectedComponents.js +1 -0
  62. package/dist/packages/stats/correlation.d.ts +26 -0
  63. package/dist/packages/stats/correlation.js +1 -1
  64. package/dist/packages/stats/facets.js +1 -1
  65. package/dist/packages/stats/filterFindings.d.ts +18 -0
  66. package/dist/packages/stats/filterFindings.js +1 -1
  67. package/dist/packages/stats/findingsNarrative.js +1 -1
  68. package/dist/packages/stats/histogram.d.ts +19 -1
  69. package/dist/packages/stats/histogram.js +1 -1
  70. package/dist/packages/stats/index.d.ts +6 -0
  71. package/dist/packages/stats/index.js +1 -1
  72. package/dist/packages/stats/mergeTestDefs.d.ts +44 -0
  73. package/dist/packages/stats/mergeTestDefs.js +1 -0
  74. package/dist/packages/stats/metadataColumns.d.ts +20 -4
  75. package/dist/packages/stats/metadataColumns.js +1 -1
  76. package/dist/packages/stats/patternClassification.js +1 -1
  77. package/dist/packages/stats/renderFindingsReport.js +14 -14
  78. package/dist/packages/stats/renderSummaryReport.js +32 -29
  79. package/dist/packages/stats/reportHtml.js +38 -13
  80. package/dist/packages/stats/testPassRate.d.ts +85 -0
  81. package/dist/packages/stats/testPassRate.js +1 -0
  82. package/dist/packages/stats/trend.d.ts +40 -0
  83. package/dist/packages/stats/trend.js +1 -0
  84. package/dist/packages/stats/types.d.ts +117 -6
  85. package/llms.txt +6 -0
  86. package/package.json +7 -5
  87. package/dist/packages/canvas-adapter/metadataBadge.d.ts +0 -30
  88. 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
- export declare function applyCanvasFlow(canvas: HTMLCanvasElement, topOffset?: number): void;
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): HTMLElement;
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
- }): HTMLSelectElement;
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
- * When the Analysis tab's "Group by" is active, this panel gets its own
17
- * "Group: <value> ▾" restrict-to-one-group dropdown (matching capability's
18
- * pattern exactly) — the matrix is always computed for exactly one group's
19
- * dies, never pooled across groups. `items` above is ignored when `groups`
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 mt,filterCorrelationMatrix as ht}from"../../stats/correlation.js";import{CORRELATION_POSITIVE as st,CORRELATION_NEGATIVE as lt}from"./palette.js";import{buildFacetTable as xt}from"../../stats/facets.js";import{CLR as b}from"../toolbar.js";import{attachChartTip as gt,cardShell as bt,observeResize as yt,makeTooltip as Mt,positionChartTooltip as wt,makeLabeledSelect as Lt,makeWaferSelect as vt,renderEmptyState as Ct,resolveChartCanvasColors as It}from"./chartShell.js";const it=5,rt=100,at=20;function Z(y){const r=y.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);if(r)return[+r[1],+r[2],+r[3]];const c=y.trim().replace("#","");return c.length===6?[parseInt(c.slice(0,2),16),parseInt(c.slice(2,4),16),parseInt(c.slice(4,6),16)]:null}function Et(y){const r=Z(y);return r?(r[0]*299+r[1]*587+r[2]*114)/1e3>140?"#111":"#fff":"#000"}function Tt(y,r,c){const k=Z(y);if(!k)return y;const P=Math.round(r[0]+(k[0]-r[0])*c),m=Math.round(r[1]+(k[1]-r[1])*c),D=Math.round(r[2]+(k[2]-r[2])*c);return`rgb(${P},${m},${D})`}export function renderCorrelationPanel(y){const{title:r="Test correlation matrix",items:c,testDefs:k,onSaveImage:P,groups:m,onSelectPair:D}=y,{card:l,body:S,controlsRow:V}=bt(r,P,y.ownerDocument);S.style.overflowX="auto",l.style.alignSelf="start";let j=at,U=m&&m.length>0?m[0].key:void 0,N=null;const H=l.ownerDocument.createElement("label");H.textContent="Max tests:",Object.assign(H.style,{color:b.label,fontSize:"12px",display:"flex",alignItems:"center",gap:"4px"});const v=l.ownerDocument.createElement("input");v.type="number",v.min=String(it),v.max=String(rt),v.value=String(j),Object.assign(v.style,{width:"52px",fontSize:"12px",padding:"2px 4px",background:b.menuBg,color:b.value,border:`1px solid ${b.menuBorder}`,borderRadius:"3px"}),v.addEventListener("change",()=>{const e=Math.max(it,Math.min(rt,parseInt(v.value,10)||at));v.value=String(e),e!==j&&(j=e,X())}),H.appendChild(v),V.appendChild(H),m&&m.length>0?V.appendChild(Lt("Group:",m.map(e=>({value:e.key,label:e.key})),U??"",e=>{U=e,X()},{ownerDocument:l.ownerDocument})):c.length>1&&V.appendChild(vt(c,N,e=>{N=e,X()},{ownerDocument:l.ownerDocument}));function ct(){return m&&m.length>0?m.find(e=>e.key===U)?.items??[]:N!==null?c[N]?[c[N]]:[]:c}const A=l.ownerDocument.createElement("div");Object.assign(A.style,{display:"flex",flexDirection:"column",gap:"4px",marginBottom:"6px"}),l.insertBefore(A,S);const B=window.devicePixelRatio||1,I=Mt(l);gt(H,l,I,"Cap on how many tests the matrix includes (strongest correlations kept first)");let q=()=>{};function dt(e,s,d,f,E){if(A.innerHTML="",E.length>0){const h=l.ownerDocument.createElement("div");h.textContent=`\u26A0 Mixed ${E.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(h.style,{color:b.warnText,background:b.warnBg,border:`1px solid ${b.warnBorder}`,borderRadius:"4px",padding:"4px 8px",fontSize:"11px"}),A.appendChild(h)}const a=l.ownerDocument.createElement("span");Object.assign(a.style,{display:"inline-flex",alignItems:"center",gap:"6px",color:b.label,fontSize:"11px",flexWrap:"wrap"});const T=l.ownerDocument.createElement("span");T.textContent="Pearson r \xB7 click a cell to view that pair in scatter \xB7",a.appendChild(T);const p=l.ownerDocument.createElement("span");Object.assign(p.style,{display:"inline-flex",alignItems:"center",gap:"4px"});const _=l.ownerDocument.createElement("span");_.textContent="\u22121";const M=l.ownerDocument.createElement("span");Object.assign(M.style,{display:"inline-block",width:"64px",height:"8px",borderRadius:"2px",border:`1px solid ${b.menuBorder}`,background:`linear-gradient(to right, ${lt}, ${b.menuBg}, ${st})`});const z=l.ownerDocument.createElement("span");z.textContent="+1",p.append(_,M,z),a.appendChild(p),A.appendChild(a);const $=l.ownerDocument.createElement("span");if(Object.assign($.style,{color:b.value,fontSize:"12px",fontWeight:"500"}),e===0&&s===0)$.textContent=f?`No significant correlations found \u2014 strongest pair: ${f.xLabel} \u2194 ${f.yLabel} (r = ${f.r.toFixed(2)})`:"No significant correlations found";else{const h=[];e>0&&h.push(`${e} strong (|r| \u2265 0.7)`),s>0&&h.push(`${s} moderate (0.4\u20130.7)`);const F=e+s;let W=h.join(", ")+` pair${F!==1?"s":""} found`;d>0&&(W+=` \xB7 ${d} weak pair${d!==1?"s":""} not shown`),$.textContent=W}A.appendChild($)}function pt(e){if(S.innerHTML="",e.tests.length<2)return Ct(S,"Need at least two parametric tests for a correlation matrix."),()=>{};const s=l.ownerDocument.createElement("canvas");s.style.display="block",s.style.cursor="default",S.appendChild(s);const d=e.tests.length,f=o=>o.label.split(" (#")[0],E=Math.min(14,Math.max(...e.tests.map(o=>f(o).length))),a=E*6.5+8,T=10,p=Math.round(T*6.5*Math.sin(Math.PI/4))+14,_=14,M=28,z=44;let $=-1,h=-1;function F(o){const i=Math.max(0,o-a);return Math.max(_,Math.min(z,Math.floor(i/d)))}const W=new Map;for(const o of e.cells){let i=W.get(o.yIndex);i||(i=[],W.set(o.yIndex,i)),i.push(o)}function tt(){const o=It(l),i=Z(o.bg)??[255,255,255],u=S.clientWidth,n=F(u),O=n*d,C=p+n*d+4,w=a+O;s.width=Math.max(1,Math.floor(w*B)),s.height=Math.max(1,Math.floor(C*B)),s.style.width=`${w}px`,s.style.height=`${C}px`;const t=s.getContext("2d");t.setTransform(B,0,0,B,0,0),t.clearRect(0,0,w,C),t.font="10px system-ui, sans-serif",t.fillStyle=o.text,e.tests.forEach((x,g)=>{const L=f(x),R=L.length>T?`${L.slice(0,T-1)}\u2026`:L,J=a+g*n+n/2;t.save(),t.translate(J,p-4),t.rotate(-Math.PI/4),t.textAlign="left",t.textBaseline="bottom",t.fillStyle=g===$?o.text:o.textMuted,t.fillText(R,0,0),t.restore()}),e.tests.forEach((x,g)=>{const L=f(x),R=p+g*n,J=R+n/2;t.font="10px system-ui, sans-serif",t.textAlign="right",t.textBaseline="middle",t.fillStyle=g===h?o.text:o.textMuted,t.fillText(L.length>E?`${L.slice(0,E-1)}\u2026`:L,a-4,J),(W.get(g)??[]).forEach(nt=>{const K=nt.xIndex,G=a+K*n,ft=K===$&&g===h,ot=K===g,Y=nt.r;if(Y===null){t.fillStyle=o.border,t.globalAlpha=.4,t.fillRect(G+1,R+1,n-2,n-2),t.globalAlpha=1;return}let Q=o.bgHover;ot?t.fillStyle=o.bgHover:(Q=Tt(Y>=0?st:lt,i,Math.abs(Y)),t.fillStyle=Q),t.fillRect(G+1,R+1,n-2,n-2),n>=M&&!ot&&(t.font=`${Math.min(10,n*.35)}px system-ui, sans-serif`,t.fillStyle=Et(Q),t.textAlign="center",t.textBaseline="middle",t.fillText(Y.toFixed(2),G+n/2,R+n/2)),ft&&(t.strokeStyle=o.text,t.lineWidth=2,t.strokeRect(G+1,R+1,n-2,n-2),t.lineWidth=1)})}),t.strokeStyle=o.bg,t.lineWidth=1;for(let x=0;x<=d;x++){const g=a+x*n;t.beginPath(),t.moveTo(g,p),t.lineTo(g,p+d*n),t.stroke();const L=p+x*n;t.beginPath(),t.moveTo(a,L),t.lineTo(a+d*n,L),t.stroke()}}function et(o){const i=s.getBoundingClientRect(),u=S.clientWidth,n=F(u),O=(o.clientX-i.left)*(s.width/B/i.width),C=(o.clientY-i.top)*(s.height/B/i.height),w=Math.floor((O-a)/n),t=Math.floor((C-p)/n);return w>=0&&w<d&&t>=0&&t<d?{xi:w,yi:t}:null}return s.addEventListener("mousemove",o=>{const i=et(o);if(!i){I.style.display="none",s.style.cursor="default";return}const{xi:u,yi:n}=i,O=u===n;s.style.cursor=O||!D?"default":"pointer";const C=e.cells.find(x=>x.xIndex===u&&x.yIndex===n),w=e.tests[u].label,t=e.tests[n].label;O?I.innerHTML=`<strong>${w}</strong>`:C?.r!==null&&C?.r!==void 0?I.innerHTML=`<strong>${f(e.tests[n])}</strong> (#${e.tests[n].testNumber}) vs <strong>${f(e.tests[u])}</strong> (#${e.tests[u].testNumber})<br>r = ${C.r.toFixed(4)}${D?"<br><em>click to view in scatter</em>":""}`:I.innerHTML=`${t} vs ${w}<br><em>insufficient data</em>`,I.style.display="block",wt(I,l,o.clientX,o.clientY)}),s.addEventListener("mouseleave",()=>{I.style.display="none"}),s.addEventListener("click",o=>{const i=et(o);if(!i||!D)return;const{xi:u,yi:n}=i;u!==n&&($=u,h=n,tt(),D(e.tests[u].testNumber,e.tests[n].testNumber))}),tt}function X(){const e=ct(),s=e.flatMap(M=>M.dies??[]),d=mt(s,k),{matrix:f,strongPairs:E,moderatePairs:a,hiddenWeakPairs:T,strongestPair:p}=ht(d,{minTests:6,maxTests:j}),_=xt(e,{facetableOnly:!0}).filter(M=>M.splittable).map(M=>M.label);dt(E,a,T,p,_),q=pt(f),q()}const ut=yt(l,()=>q());return X(),{card:l,destroy:()=>ut.disconnect()}}
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)}}}