@wafertools/wafermap 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -7
- package/CHANGELOG.md +374 -1
- package/README.md +5 -5
- package/dist/packages/canvas-adapter/chartPopulation.d.ts +15 -10
- package/dist/packages/canvas-adapter/chartPopulation.js +1 -1
- package/dist/packages/canvas-adapter/charts/barPanel.d.ts +7 -0
- package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
- package/dist/packages/canvas-adapter/charts/binCluster.d.ts +7 -1
- package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
- package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
- package/dist/packages/canvas-adapter/charts/capability.js +1 -1
- package/dist/packages/canvas-adapter/charts/chartShell.d.ts +57 -15
- package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
- package/dist/packages/canvas-adapter/charts/correlation.js +1 -2
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +2 -0
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
- package/dist/packages/canvas-adapter/charts/histogram.d.ts +16 -0
- package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
- package/dist/packages/canvas-adapter/charts/palette.d.ts +2 -0
- package/dist/packages/canvas-adapter/charts/palette.js +1 -1
- package/dist/packages/canvas-adapter/charts/plotChart.d.ts +27 -0
- package/dist/packages/canvas-adapter/charts/plotChart.js +1 -0
- package/dist/packages/canvas-adapter/charts/pointInteractions.d.ts +45 -0
- package/dist/packages/canvas-adapter/charts/pointInteractions.js +1 -0
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +2 -0
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
- package/dist/packages/canvas-adapter/charts/scatter.d.ts +19 -0
- package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
- package/dist/packages/canvas-adapter/charts/sweep.d.ts +12 -0
- package/dist/packages/canvas-adapter/charts/sweep.js +1 -1
- package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +7 -0
- package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
- package/dist/packages/canvas-adapter/charts/trend.d.ts +11 -0
- package/dist/packages/canvas-adapter/charts/trend.js +1 -1
- package/dist/packages/canvas-adapter/confirmDialog.d.ts +13 -0
- package/dist/packages/canvas-adapter/confirmDialog.js +1 -0
- package/dist/packages/canvas-adapter/dataTab.d.ts +131 -0
- package/dist/packages/canvas-adapter/dataTab.js +1 -0
- package/dist/packages/canvas-adapter/dieList.d.ts +39 -4
- package/dist/packages/canvas-adapter/dieList.js +6 -7
- package/dist/packages/canvas-adapter/drilldown.d.ts +20 -3
- package/dist/packages/canvas-adapter/drilldown.js +1 -1
- package/dist/packages/canvas-adapter/guideWindow.d.ts +96 -0
- package/dist/packages/canvas-adapter/guideWindow.js +1 -0
- package/dist/packages/canvas-adapter/identityHeader.d.ts +16 -0
- package/dist/packages/canvas-adapter/identityHeader.js +1 -1
- package/dist/packages/canvas-adapter/index.d.ts +3 -3
- package/dist/packages/canvas-adapter/index.js +1 -1
- package/dist/packages/canvas-adapter/insightsTab.d.ts +73 -54
- package/dist/packages/canvas-adapter/insightsTab.js +1 -1
- package/dist/packages/canvas-adapter/maplessSummary.d.ts +2 -2
- package/dist/packages/canvas-adapter/plotEditor.d.ts +29 -0
- package/dist/packages/canvas-adapter/plotEditor.js +1 -0
- package/dist/packages/canvas-adapter/plotItems.d.ts +32 -0
- package/dist/packages/canvas-adapter/plotItems.js +1 -0
- package/dist/packages/canvas-adapter/plotModal.d.ts +30 -0
- package/dist/packages/canvas-adapter/plotModal.js +1 -0
- package/dist/packages/canvas-adapter/plotStore.d.ts +52 -0
- package/dist/packages/canvas-adapter/plotStore.js +1 -0
- package/dist/packages/canvas-adapter/plotTab.d.ts +30 -0
- package/dist/packages/canvas-adapter/plotTab.js +1 -0
- package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +3 -2
- package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +51 -2
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +49 -11
- package/dist/packages/canvas-adapter/summaryPanel.js +2 -3
- package/dist/packages/canvas-adapter/summaryRail.d.ts +7 -0
- package/dist/packages/canvas-adapter/summaryRail.js +1 -0
- package/dist/packages/canvas-adapter/sweepEditor.d.ts +17 -0
- package/dist/packages/canvas-adapter/sweepEditor.js +1 -0
- package/dist/packages/canvas-adapter/tableExport.d.ts +22 -0
- package/dist/packages/canvas-adapter/tableExport.js +3 -0
- package/dist/packages/canvas-adapter/toCanvas.js +1 -1
- package/dist/packages/canvas-adapter/toolbar.d.ts +74 -93
- 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 +409 -51
- package/dist/packages/canvas-adapter/version.d.ts +2 -2
- package/dist/packages/canvas-adapter/version.js +1 -1
- package/dist/packages/canvas-adapter/viewSwitch.d.ts +13 -0
- package/dist/packages/canvas-adapter/viewSwitch.js +1 -0
- package/dist/packages/canvas-adapter/virtualTable.d.ts +44 -0
- package/dist/packages/canvas-adapter/virtualTable.js +1 -0
- package/dist/packages/core/aggregates.d.ts +6 -0
- package/dist/packages/core/aggregates.js +1 -1
- package/dist/packages/core/classify.d.ts +3 -2
- package/dist/packages/core/classify.js +1 -1
- package/dist/packages/core/compact.d.ts +205 -0
- package/dist/packages/core/compact.js +3 -0
- package/dist/packages/core/passBins.d.ts +23 -4
- package/dist/packages/core/passBins.js +1 -1
- package/dist/packages/core/ringCount.d.ts +22 -0
- package/dist/packages/core/ringCount.js +1 -0
- package/dist/packages/core/tableCsv.d.ts +43 -0
- package/dist/packages/core/tableCsv.js +3 -0
- package/dist/packages/renderer/axisTicks.d.ts +14 -0
- package/dist/packages/renderer/axisTicks.js +1 -1
- package/dist/packages/renderer/binColors.d.ts +6 -6
- package/dist/packages/renderer/binColors.js +1 -1
- package/dist/packages/renderer/buildView.d.ts +27 -2
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/renderer/compactView.d.ts +22 -0
- package/dist/packages/renderer/compactView.js +1 -0
- package/dist/packages/renderer/xyIndicator.d.ts +27 -0
- package/dist/packages/renderer/xyIndicator.js +1 -0
- package/dist/packages/stats/analyzeWaferLot.d.ts +28 -0
- package/dist/packages/stats/analyzeWaferLot.js +1 -1
- package/dist/packages/stats/analyzeWaferMap.d.ts +36 -1
- package/dist/packages/stats/analyzeWaferMap.js +1 -1
- package/dist/packages/stats/binPareto.d.ts +1 -1
- package/dist/packages/stats/binPareto.js +1 -1
- package/dist/packages/stats/binRows.d.ts +38 -0
- package/dist/packages/stats/binRows.js +1 -0
- package/dist/packages/stats/filterFindings.d.ts +25 -0
- package/dist/packages/stats/filterFindings.js +1 -1
- package/dist/packages/stats/findingText.d.ts +9 -0
- package/dist/packages/stats/findingText.js +2 -0
- package/dist/packages/stats/histogram.d.ts +27 -0
- package/dist/packages/stats/histogram.js +1 -1
- package/dist/packages/stats/index.d.ts +2 -0
- package/dist/packages/stats/index.js +1 -1
- package/dist/packages/stats/lotDrift.d.ts +16 -0
- package/dist/packages/stats/lotDrift.js +1 -0
- package/dist/packages/stats/lotPattern.d.ts +33 -0
- package/dist/packages/stats/lotPattern.js +1 -0
- package/dist/packages/stats/math.d.ts +60 -6
- package/dist/packages/stats/math.js +1 -1
- package/dist/packages/stats/patternClassification.d.ts +32 -1
- package/dist/packages/stats/patternClassification.js +1 -1
- package/dist/packages/stats/plotData.d.ts +258 -0
- package/dist/packages/stats/plotData.js +1 -0
- package/dist/packages/stats/plotId.d.ts +3 -0
- package/dist/packages/stats/plotId.js +1 -0
- package/dist/packages/stats/plotSpec.d.ts +119 -0
- package/dist/packages/stats/plotSpec.js +4 -0
- package/dist/packages/stats/presentation.d.ts +44 -0
- package/dist/packages/stats/presentation.js +1 -0
- package/dist/packages/stats/regions.d.ts +15 -0
- package/dist/packages/stats/regions.js +1 -1
- package/dist/packages/stats/renderSummaryReport.d.ts +23 -5
- package/dist/packages/stats/renderSummaryReport.js +32 -27
- package/dist/packages/stats/reportHtml.d.ts +54 -9
- package/dist/packages/stats/reportHtml.js +145 -55
- package/dist/packages/stats/scatter.d.ts +6 -0
- package/dist/packages/stats/scatter.js +1 -1
- package/dist/packages/stats/sweep.d.ts +37 -1
- package/dist/packages/stats/sweep.js +1 -1
- package/dist/packages/stats/sweepText.d.ts +14 -0
- package/dist/packages/stats/sweepText.js +1 -0
- package/dist/packages/stats/sweepXFromName.d.ts +14 -0
- package/dist/packages/stats/sweepXFromName.js +1 -1
- package/dist/packages/stats/synthesis.d.ts +76 -0
- package/dist/packages/stats/synthesis.js +2 -0
- package/dist/packages/stats/testPassRate.d.ts +6 -0
- package/dist/packages/stats/testPassRate.js +1 -1
- package/dist/packages/stats/trend.js +1 -1
- package/dist/packages/stats/types.d.ts +2 -2
- package/dist/packages/stats/yield.d.ts +15 -6
- package/dist/packages/stats/yield.js +1 -1
- package/dist/packages/stats/yieldRows.d.ts +31 -0
- package/dist/packages/stats/yieldRows.js +1 -0
- package/llms.txt +16 -4
- package/package.json +3 -3
- package/dist/packages/stats/findingsNarrative.d.ts +0 -3
- package/dist/packages/stats/findingsNarrative.js +0 -1
|
@@ -1,31 +1,37 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Die } from '../core/dies.js';
|
|
2
|
+
import type { Wafer } from '../core/wafer.js';
|
|
3
|
+
import type { LotStatsSummary, StatsSummary } from '../stats/types.js';
|
|
4
|
+
import { type FacetItem } from '../stats/facets.js';
|
|
5
|
+
import { type TestDef } from '../renderer/buildWaferMap.js';
|
|
2
6
|
import type { WaferMapDisplayItem } from './renderWaferGallery.js';
|
|
7
|
+
import { type PlotStore } from './plotStore.js';
|
|
8
|
+
import type { PlotSpec } from '../stats/plotSpec.js';
|
|
3
9
|
import type { BinColors } from '../renderer/binColors.js';
|
|
4
10
|
import { type SaveImageHandler, type SaveTextHandler } from './toolbar.js';
|
|
5
|
-
import {
|
|
6
|
-
export type InsightsView = 'overview' | 'distributions' | 'correlation' | 'sweeps';
|
|
11
|
+
import type { SweepSpec } from '../stats/sweep.js';
|
|
12
|
+
export type InsightsView = 'overview' | 'distributions' | 'correlation' | 'sweeps' | 'data' | 'plot';
|
|
7
13
|
/** Public option shape for `RenderOptions.insights`/`GalleryOptions.insights`. */
|
|
8
14
|
export interface InsightsOptions {
|
|
9
15
|
/**
|
|
10
|
-
* Show an "Insights" tab in the toolbar. Selecting it replaces the canvas/
|
|
16
|
+
* Show an "Insights" tab in the toolbar and a Maps | Insights switch. Selecting it replaces the canvas/
|
|
11
17
|
* grid with wmap's own chart suite across three sub-tabs — Overview
|
|
12
|
-
* (yield, bins, ring/quadrant yield
|
|
18
|
+
* (yield, bins, ring/quadrant yield), Distributions
|
|
13
19
|
* (process capability, boxplot, histogram), and Correlation (matrix +
|
|
14
|
-
* scatter) —
|
|
20
|
+
* scatter) — a Data tab (the scope as tables: statistics, dies, wafers, with CSV
|
|
21
|
+
* export) and a Plot tab, where the reader's own plots and sweeps live. Default true with a toolbar
|
|
22
|
+
* (a gallery always has one); `false` opts out.
|
|
15
23
|
*/
|
|
16
24
|
enabled?: boolean;
|
|
17
|
-
/** Which sub-tab is shown first. Default 'overview'. `'sweeps'`
|
|
18
|
-
* `sweeps` defined falls back to 'overview' — there is no such tab to open. */
|
|
25
|
+
/** Which sub-tab is shown first. Default 'overview'. `'sweeps'` opens the Plot tab, where sweeps are drawn. */
|
|
19
26
|
defaultView?: InsightsView;
|
|
20
27
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
28
|
+
* @deprecated Sweeps are plots now: give them in `plots` as sweep plots, or read a sweeps file with
|
|
29
|
+
* `readPlotsFile`. Each entry here is still drawn, as a sweep plot on the Plot tab (and offered on selected dies),
|
|
30
|
+
* under the same `id`; a plot saved under that id takes precedence. The first use logs a notice.
|
|
23
31
|
*
|
|
24
|
-
* A sweep reads an ordered run of tests as a response curve rather than as
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* same quantity measured at a series of power levels, recorded as a block of
|
|
28
|
-
* consecutive test numbers, swept up in one block and down in another.
|
|
32
|
+
* A sweep reads an ordered run of tests as a response curve rather than as independent tests, and measures the
|
|
33
|
+
* PAIR: where the first two series cross, and how far apart they are at given levels. It carries no population:
|
|
34
|
+
* the dies it aggregates are whatever the view is scoped to or the reader selected.
|
|
29
35
|
*
|
|
30
36
|
* ```ts
|
|
31
37
|
* insights: { enabled: true, sweeps: [{
|
|
@@ -38,41 +44,32 @@ export interface InsightsOptions {
|
|
|
38
44
|
* }] }
|
|
39
45
|
* ```
|
|
40
46
|
*
|
|
41
|
-
*
|
|
42
|
-
* says which tests form the curve, and is therefore valid for any population
|
|
43
|
-
* and portable between hosts. The dies it aggregates are whatever the
|
|
44
|
-
* Insights view is currently scoped to.
|
|
45
|
-
*
|
|
46
|
-
* The same definitions drive drilldown: a user can select dies on a map and
|
|
47
|
-
* right-click (or use the toolbar's "Chart the selection") to sweep just
|
|
48
|
-
* those dies. A single map offers this even when `enabled` is off.
|
|
49
|
-
*
|
|
50
|
-
* **Provisional.** This arrived as one site's request. The mechanism — an
|
|
51
|
-
* ordered run of tests read as a curve — is a recurring semiconductor shape
|
|
52
|
-
* (shmoo, VDD/temperature sweeps, retention, endurance, IV), which is the case
|
|
53
|
-
* for it being library-level; against it is that only one host has asked. It
|
|
54
|
-
* is shipping so tsmap can put it in front of that user, and the answer
|
|
55
|
-
* decides it: a second sweep-shaped use means it is general and stays, while
|
|
56
|
-
* "also measure X, and split by Y" means it is a bespoke chart and belongs
|
|
57
|
-
* behind a host-contributed-panel extension point instead. `separationAt` is
|
|
58
|
-
* the narrowest part of the surface and the first thing to drop if it is not
|
|
59
|
-
* used. Do not widen this shape before that question is settled.
|
|
60
|
-
*
|
|
61
|
-
* Range strings in `tests` (`"1010..1030"`) are NOT a widening in that sense:
|
|
62
|
-
* they are input syntax for the same list of tests, and add nothing to what a
|
|
63
|
-
* sweep measures. They were added because ranges are the first thing anyone
|
|
64
|
-
* writing a sweep by hand asks for, and they share the derived-test parser so
|
|
65
|
-
* the syntax is one syntax library-wide.
|
|
47
|
+
* Range strings in `tests` (`"1010..1030"`) share the derived-test parser, so the syntax is one syntax library-wide.
|
|
66
48
|
*/
|
|
67
49
|
sweeps?: SweepSpec[];
|
|
68
50
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* with the remaining `sweeps`. Without it the notice still shows, with no
|
|
73
|
-
* button. The library never edits the host's sweep list itself.
|
|
51
|
+
* @deprecated With `sweeps`. Called with the id of a sweep from `sweeps` that the reader deletes on the Plot tab, so the
|
|
52
|
+
* host stops supplying it (otherwise the next render brings it back). Plots deleted from `plots` arrive through
|
|
53
|
+
* `onPlotsChange` like any other.
|
|
74
54
|
*/
|
|
75
55
|
onRemoveSweeps?: (ids: string[]) => void;
|
|
56
|
+
/**
|
|
57
|
+
* The reader's saved plots: the recipes behind the **Plot** tab, in the order the reader keeps them. The
|
|
58
|
+
* library never stores anything itself: hand the list in here (from a file, `localStorage`, a server) and keep
|
|
59
|
+
* it current from `onPlotsChange`. A plot carries no population, so the same list works on any lot; one that
|
|
60
|
+
* needs a test the open lot lacks is kept and shown greyed with the reason, never dropped.
|
|
61
|
+
*/
|
|
62
|
+
plots?: PlotSpec[];
|
|
63
|
+
/**
|
|
64
|
+
* Called with the whole list after the reader adds, edits, duplicates, deletes or imports a plot (a burst of
|
|
65
|
+
* typing is one call). Keep it, and pass it back as `plots` next time.
|
|
66
|
+
*/
|
|
67
|
+
onPlotsChange?: (plots: PlotSpec[]) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Choose a plots file to import, resolving to its text, or `null` when cancelled. Without it the Plot tab uses
|
|
70
|
+
* the browser's own file input; a desktop host passes its native dialog.
|
|
71
|
+
*/
|
|
72
|
+
onPickPlotsFile?: () => Promise<string | null>;
|
|
76
73
|
/**
|
|
77
74
|
* Open the Insights view on mount instead of starting on the map/grid.
|
|
78
75
|
* Default false — the tab is offered, the map is what you see first.
|
|
@@ -97,7 +94,7 @@ export interface InsightsTabDeps {
|
|
|
97
94
|
*/
|
|
98
95
|
getBinColors: () => BinColors;
|
|
99
96
|
/** Read fresh each render — used by the Overview tab's ring/quadrant regional yield cards. Default 4. */
|
|
100
|
-
getRingCount
|
|
97
|
+
getRingCount: () => number;
|
|
101
98
|
onSaveImage?: SaveImageHandler;
|
|
102
99
|
/** Optional host hook for the Overview tab's test-values "Export CSV" button — see `saveTextFile` (toolbar.ts). */
|
|
103
100
|
onSaveText?: SaveTextHandler;
|
|
@@ -127,6 +124,11 @@ export interface InsightsTabDeps {
|
|
|
127
124
|
* lands on this test), and only consulted for a leaf row of the sole item.
|
|
128
125
|
*/
|
|
129
126
|
focusTest?: (testNumber: number) => void;
|
|
127
|
+
/**
|
|
128
|
+
* A die row clicked in a table (the Data tab, or a table opened from a chart): leave Insights and show
|
|
129
|
+
* that die on its map. Omit where there is no map to go back to; the rows are then not clickable.
|
|
130
|
+
*/
|
|
131
|
+
locateDie?: (die: Die, waferIndex: number | undefined) => void;
|
|
130
132
|
/** Default sub-tab shown on first render. Default 'overview'. */
|
|
131
133
|
defaultView?: InsightsView;
|
|
132
134
|
/** Sweep definitions to render in the Distributions view — see `InsightsOptions.sweeps`. */
|
|
@@ -134,15 +136,13 @@ export interface InsightsTabDeps {
|
|
|
134
136
|
/** See `InsightsOptions.onRemoveSweeps`. */
|
|
135
137
|
onRemoveSweeps?: (ids: string[]) => void;
|
|
136
138
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* model (a tab row) instead of relying on the host toolbar's icon-swap
|
|
140
|
-
* toggle alone, whose "way back" is discoverable only via tooltip.
|
|
139
|
+
* The live list of saved plots, shared with the drilldown menu so a plot edited here is the one a right-click
|
|
140
|
+
* opens. Created from `plots`/`onPlotsChange` when omitted.
|
|
141
141
|
*/
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
plotStore?: PlotStore;
|
|
143
|
+
plots?: PlotSpec[];
|
|
144
|
+
onPlotsChange?: (plots: PlotSpec[]) => void;
|
|
145
|
+
onPickPlotsFile?: () => Promise<string | null>;
|
|
146
146
|
/** Opens the user guide. Rendered as an icon at the end of the tab row —
|
|
147
147
|
* while Insights is showing, the map toolbar is hidden (it held only a
|
|
148
148
|
* back-to-gallery button, which the back tab already provides, and this
|
|
@@ -184,5 +184,24 @@ export interface InsightsTabHandle {
|
|
|
184
184
|
/** Tears down every live panel's observers and removes `el`. */
|
|
185
185
|
destroy: () => void;
|
|
186
186
|
}
|
|
187
|
+
type Item = FacetItem & {
|
|
188
|
+
dies: Die[];
|
|
189
|
+
label: string;
|
|
190
|
+
waferIndex: number;
|
|
191
|
+
wafer: Wafer;
|
|
192
|
+
statsSummary?: StatsSummary;
|
|
193
|
+
/** This wafer's OWN pass bins — a lot can mix wafers built with different ones. */
|
|
194
|
+
passBins: readonly number[];
|
|
195
|
+
/** This wafer's OWN test defs, carried through so a scoped population can be
|
|
196
|
+
* reconciled (`mergeTestDefs`) over just the wafers in scope. */
|
|
197
|
+
testDefs?: TestDef[];
|
|
198
|
+
/** The wafer's real identity (`waferIdentityLabel`) — never the positional
|
|
199
|
+
* "Wafer N (no ID)" that `label` falls back to, which in a chart title would
|
|
200
|
+
* read as an ID. */
|
|
201
|
+
identity?: string;
|
|
202
|
+
};
|
|
203
|
+
/** One wafer (or the lot's single stand-in) as the Insights views see it. */
|
|
204
|
+
export type InsightsItem = Item;
|
|
187
205
|
export declare function createInsightsTab(deps: InsightsTabDeps): InsightsTabHandle;
|
|
206
|
+
export {};
|
|
188
207
|
//# sourceMappingURL=insightsTab.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildFacetTable as He,facetValueOf as Fe,FACET_NONE_VALUE as Ne}from"../stats/facets.js";import{mergeTestDefs as de}from"../stats/mergeTestDefs.js";import{isParametricTest as Ve}from"../renderer/buildWaferMap.js";import{waferDisplayLabel as _e,waferIdentityLabel as ze}from"../core/waferLabel.js";import{hasDrilldownTargets as ce,waferPopulation as Ue}from"./chartPopulation.js";import{openDrilldownMenu as qe}from"./drilldown.js";import{INPUT_DEFAULT_PASS_BINS as M,itemPassBins as Qe,passBinsLabel as pe}from"../core/passBins.js";import{NO_DATA_FILL as ue}from"../renderer/colorMap.js";import{describeWaferPopulation as Ke,populationStat as Xe}from"../stats/population.js";import{LEADING as fe,ALPHA as Je,SPACE as w,EDGE_GUTTER as te,FONT as A,CLR as k,RADIUS as me,SHADOW as Ze,controlStyle as be,wireControlHover as ne,wireTooltip as et}from"./toolbar.js";import{ICONS as tt}from"./icons.js";import{renderCapabilityPanel as nt}from"./charts/capability.js";import{renderBoxplotPanel as at}from"./charts/boxplot.js";import{renderTrendPanel as ot}from"./charts/trend.js";import{renderSweepPanel as st}from"./charts/sweep.js";import{sweepAppliesTo as rt}from"../stats/sweep.js";import{renderHistogramPanel as it}from"./charts/histogram.js";import{renderCorrelationPanel as lt}from"./charts/correlation.js";import{renderScatterPanel as dt}from"./charts/scatter.js";import{renderBarPanel as ge}from"./charts/barPanel.js";import{renderBinClusterPanel as ct}from"./charts/binCluster.js";import{renderTestPassRatePanel as pt}from"./charts/testPassRate.js";import{QUANTITY as ut}from"./charts/palette.js";import{cardFrameStyle as ft,makeChartGridWrap as _,makeLabeledSelect as mt,makeLinkedGroupSelect as bt}from"./charts/chartShell.js";import{buildYieldData as z,buildYieldDataCombined as he}from"../stats/yield.js";import{buildBinParetoData as gt}from"../stats/binPareto.js";import{buildLotTestSectionSteps as ht,buildLotFunctionalSection as yt,buildMetadataStripBox as wt}from"./summaryPanel.js";import{runChunked as Ct}from"./chunked.js";import{buildRegionYieldData as ye,buildRingRegions as xt,buildQuadrantRegions as St}from"../stats/regions.js";import{renderRegionYieldDiagram as we}from"./charts/regionYieldDiagram.js";const Ce=[{key:"overview",label:"Overview"},{key:"distributions",label:"Distributions"},{key:"correlation",label:"Correlation"}],vt={key:"sweeps",label:"Sweeps"};export function createInsightsTab(x){const{getItems:xe,getLotStats:Se,getBinColors:ae,getRingCount:ve,onSaveImage:T,onSaveText:U,openWafer:F,focusTest:oe}=x,Te=x.showMetadataStrip??!0,Y=x.contentInset??te,p=x.ownerDocument??document,R=p.createElement("div");Object.assign(R.style,{display:"none",flexDirection:"column",width:"100%",paddingTop:"0"});const ke=te,W=p.createElement("div");Object.assign(W.style,{paddingLeft:ke});const $=p.createElement("div");Object.assign($.style,{display:"flex",gap:w.xs,alignItems:"center",background:k.menuBg,border:`1px solid ${k.menuBorder}`,borderRadius:me.container,boxShadow:Ze.panel,padding:`0 ${w.lg}`,marginBottom:w.lg,marginLeft:Y,marginRight:Y}),$.setAttribute("role","tablist"),$.addEventListener("keydown",e=>{if(e.key!=="ArrowLeft"&&e.key!=="ArrowRight")return;const t=Array.from($.querySelectorAll('[role="tab"]')),a=t.indexOf(p.activeElement);if(a===-1)return;e.preventDefault();const s=t[(a+(e.key==="ArrowRight"?1:-1)+t.length)%t.length];s.focus(),s.click()});const G=p.createElement("div");Object.assign(G.style,{display:"flex",flexDirection:"column",gap:w.lg,marginTop:"0",paddingLeft:Y,paddingRight:Y}),R.appendChild(W),R.appendChild($),R.appendChild(G);const se=(x.sweeps?.length??0)>0,De=se?[...Ce,vt]:Ce;let P=x.defaultView==="sweeps"&&!se?"overview":x.defaultView??"overview",j,E=null,L=null,H={includeLimits:void 0,clipOutliers:!1,limits:"both"};const q=new Map,re=(e,t)=>{H=e;for(const a of q.values())for(const s of a)s!==t&&s.setAxisPrefs(e)},Q=e=>{L!==e&&(L=e,V())};let N=new Map;function ie(){for(const e of N.values())e.destroy();N=new Map}function K(e,t,a){F?.(e,t,a)}function X(e){if(e.some(t=>ce(t.testDefs,x.sweeps)))return(t,a,s)=>{const r=e.find(u=>u.waferIndex===t);if(!r||!ce(r.testDefs,x.sweeps))return;s.preventDefault();const i=Ue(r.dies,{waferLabel:r.identity,testDefs:r.testDefs,activeTest:a??E??void 0,waferIndex:t});qe({x:s.clientX,y:s.clientY},s.target,i,{sweeps:x.sweeps,onSaveImage:T})}}function Be(e){return F?{open:(t,a)=>K(t,`Wafer ${e.find(s=>s.waferIndex===t)?.label??t}`,a)}:oe&&e.length===1?{open:(t,a)=>oe(a),label:"show this test on the map"}:null}function $e(){return xe().map((e,t)=>e==null?null:{metadata:e.wafer.metadata??void 0,dies:e.dies,wafer:e.wafer,label:_e(e,t),identity:ze(e),waferIndex:t,passBins:Qe(e),statsSummary:e.statsSummary,testDefs:e.testDefs}).filter(e=>e!=null)}function le(e,t){Object.assign(e.style,{background:"none",border:"none",borderBottom:t?`2px solid ${k.iconActive}`:"2px solid transparent",color:t?k.iconActive:k.label,fontWeight:t?"700":"500",fontSize:A.body,padding:`${w.sm} ${te} ${w.md}`,cursor:"pointer",marginBottom:"-1px"})}function Ee(e,t){const a=p.createElement("button");a.type="button",a.textContent=t;const s=e===P;return le(a,s),a.dataset.on=s?"true":"false",ne(a,"bare"),a.dataset.wmapInsightsTab=e,a.setAttribute("role","tab"),a.setAttribute("aria-selected",s?"true":"false"),a.tabIndex=s?0:-1,a.addEventListener("click",()=>{P!==e&&(P=e,V({keepSections:!0}))}),a}function Le(e){const t=p.createElement("button");return t.type="button",t.textContent=`\u2039 ${e.label}`,le(t,!1),t.dataset.wmapInsightsBack="1",t.addEventListener("click",e.onBack),t}function Pe(e,t,a,s=!0){const r=_(p),i=ae();let u="label";const f=a??"group",g=pe(e.map(d=>d.passBins)),c=Se?.(),m=new Map(c?.lotYieldSeries.map(d=>[d.waferIndex,d.yieldPercent])),h=d=>({...d,label:d.label.replace(/\s*·\s*\d+(\.\d+)?%$/,""),yieldPercent:m.get(d.waferIndex),key:d.waferIndex}),b=e.map(h),y=t?.map(d=>({key:d.key,items:d.items.map(h)})),S=()=>y?he(y,M,u):z(b,M,u),o={title:t?`Yield by ${f} (pass: ${g})`:`Yield by wafer (pass: ${g})`,data:S(),selfControl:{current:u,options:[["yield","Sort: yield"],["label",t?`Sort: ${f}`:"Sort: wafer ID"]],onChange:d=>(u=d,{data:S()})},barColor:()=>ut,valueLabel:d=>`${d.percent.toFixed(1)}%`,reference:d=>{if(d.length<3)return null;const D=d.map(ee=>ee.percent).sort((ee,Ge)=>ee-Ge),I=D.length%2?D[(D.length-1)/2]:(D[D.length/2-1]+D[D.length/2])/2;return{value:I,label:`median ${I.toFixed(1)}%`}},drill:y?{onOpenGroup:d=>{const D=y.find(I=>I.key===d.label)?.items??[];return{data:z(D,M,u),title:`Yield by wafer \u2014 ${f}: ${d.label} (pass: ${g})`}},onBack:()=>({data:S(),title:`Yield by ${f} (pass: ${g})`}),groupLabelText:f}:void 0,onOpen:F?d=>{typeof d.key=="number"&&K(d.key,`Wafer ${d.label}`)}:void 0,onWaferContextMenu:X(e),ownerDocument:p};let n=null;if(s&&(n=ge(o,T),n.card.style.minHeight="360px",r.appendChild(n.card)),t){const d=ct({title:"Hard bin pareto",groups:t.map(D=>({key:D.key,items:D.items.map(I=>({dies:I.dies,hardBinCounts:I.statsSummary?.stats.hardBinCounts,softBinCounts:I.statsSummary?.stats.softBinCounts}))})),onSaveImage:T,ownerDocument:p});return d.card.style.minHeight="360px",r.appendChild(d.card),{card:r,destroy:()=>{n?.destroy(),d.destroy()}}}let v="hbin";const l=d=>d===void 0?ue:(v==="hbin"?i.hard:i.soft).get(d)??ue,C=e.map(d=>({dies:d.dies,hardBinCounts:d.statsSummary?.stats.hardBinCounts,softBinCounts:d.statsSummary?.stats.softBinCounts})),O=()=>gt(C,v),B={title:"Hard bin pareto",data:O(),selfControl:{current:v,options:[["hbin","Hard bins"],["sbin","Soft bins"]],onChange:d=>(v=d,{data:O(),title:`${v==="hbin"?"Hard":"Soft"} bin pareto`})},barColor:d=>l(d.binCode),ownerDocument:p},Z=ge(B,T);return Z.card.style.minHeight="360px",r.appendChild(Z.card),{card:r,destroy:()=>{n?.destroy(),Z.destroy()}}}function J(){const e=p.createElement("div");return e.dataset.wmapChartCard="1",Object.assign(e.style,{...ft(),display:"flex",flexDirection:"column",gap:w.lg,minWidth:"0"}),e}function Ie(e){if(W.innerHTML="",W.style.marginBottom="0",!Te)return;const t=wt(e.map(a=>({metadata:a.metadata})),void 0,{facetableOnly:!1});t&&(t.style.marginBottom="0",W.appendChild(t),W.style.marginBottom=w.xs)}function Oe(e,t,a,s){if(!e.length)return{elements:[],testValuesCard:null,functionalCard:null,destroy:()=>{}};const r=e.map(l=>l.wafer),i=e.map(l=>l.dies),u=e.flatMap(l=>l.dies),f=e.map(l=>l.statsSummary),g=f.every(l=>l!==void 0)?f:void 0,c=ve?.()??4,m=[],h=[],y=pt({groups:(s??[{key:"",items:e}]).map(l=>({key:l.key,items:l.items.map(C=>({dies:C.dies,testSpecYield:C.statsSummary?.stats.testSpecYield,functionalYield:C.statsSummary?.stats.functionalYield}))})),testDefs:a,onSaveImage:T,ownerDocument:p});y.card.style.minHeight="300px",m.push(y.card),h.push(y.destroy);const S=ye(i,r,c,l=>e[l].passBins,xt);if(S.length){const l=we({title:"Ring yield",mode:"ring",rows:S,onSaveImage:T,ownerDocument:p});m.push(l.card),h.push(l.destroy)}const o=ye(i,r,c,l=>e[l].passBins,St);if(o.length){const l=we({title:"Quadrant yield",mode:"quadrant",rows:o,onSaveImage:T,ownerDocument:p});m.push(l.card),h.push(l.destroy)}let n=null;if(t.length){const l=J();let C=null;const O=Ct(ht(u,t,void 0,g,U,i.map(B=>({dies:B})),void 0,"full"),{onDone:B=>{C=B,l.removeAttribute("aria-busy"),B?l.replaceChildren(B):l.remove()}});if(O.done)C&&(n=l);else{const B=p.createElement("div");B.textContent="Computing test statistics\u2026",Object.assign(B.style,{color:k.label,fontSize:A.body}),l.setAttribute("aria-busy","true"),l.appendChild(B),n=l,h.push(()=>O.cancel())}}let v=null;if(a.length){const l=yt(u,a,g,U);if(l){const C=J();C.appendChild(l),v=C}}return{elements:m,testValuesCard:n,functionalCard:v,destroy:()=>{for(const l of h)l()}}}function Ae(e){const t=pe(e.map(c=>c.passBins)),a=e.length===1,s=e[0],r=J();Object.assign(r.style,{flexDirection:"row",flexWrap:"wrap",gap:w.md});const i=c=>{const m=c.lastElementChild;return m&&Object.assign(m.style,{borderRight:"none",paddingRight:"0",marginRight:"0"}),c};function u(c,m,h){const b=p.createElement("div");Object.assign(b.style,{display:"flex",alignItems:"baseline",gap:w.sm,paddingRight:w.xxl,marginRight:w.md,borderRight:`1px solid ${k.menuBorder}`});const y=p.createElement("div");y.textContent=c,Object.assign(y.style,{fontSize:A.heading,fontWeight:"700",color:k.value,lineHeight:fe.tight});const S=p.createElement("div");if(S.textContent=m,Object.assign(S.style,{fontSize:A.body,color:k.label}),b.append(y,S),h){const o=p.createElement("div");o.textContent=h,Object.assign(o.style,{fontSize:A.meta,color:k.label,opacity:Je.muted}),b.appendChild(o)}return b}if(a){const c=z([{...s,key:s.waferIndex}],M)[0]?.percent;return c!==void 0&&r.appendChild(u(`${c.toFixed(1)}%`,`Yield \xB7 pass: ${t}`)),r.appendChild(u(String(s.dies.length),"Total dies")),i(r)}r.appendChild(u(String(e.length),"Wafers"));const f=z(e.map(c=>({...c,key:c.waferIndex})),M).map(c=>c.percent).filter(c=>Number.isFinite(c));if(f.length){const c=f.reduce((b,y)=>b+y,0)/f.length,m=he([{key:"all",items:e.map(b=>({...b,key:b.waferIndex}))}],M)[0]?.percent,h=m!==void 0&&m.toFixed(1)!==c.toFixed(1);r.appendChild(u(`${c.toFixed(1)}%`,`Mean per-wafer yield \xB7 pass: ${t}`,h?`${m.toFixed(1)}% by dies`:void 0))}const g=e.map(c=>c.statsSummary);if(g.every(c=>c!==void 0)){const c=g.reduce((h,b)=>h+b.stats.analyzedDies,0),m=g.reduce((h,b)=>h+b.stats.excludedDies,0);r.appendChild(u(c.toLocaleString(),"Dies analysed",m?`${m.toLocaleString()} excluded`:void 0))}else r.appendChild(u(e.reduce((c,m)=>c+m.dies.length,0).toLocaleString(),"Total dies"));return i(r)}function Re(e,t,a,s,r){const i=p.createElement("div");Object.assign(i.style,{display:"flex",flexDirection:"column",gap:w.lg});const u=e.length===1&&!s;e.length&&!s&&i.appendChild(Ae(e));const f=Pe(e,s,r,!u);i.appendChild(f.card);const g=Oe(e,t,a,s);for(const c of g.elements)f.card.appendChild(c);return g.testValuesCard&&i.appendChild(g.testValuesCard),g.functionalCard&&i.appendChild(g.functionalCard),{card:i,destroy:()=>{f.destroy(),g.destroy()}}}function We(e,t,a,s){const r=_(p),i=Be(e),u=o=>n=>re(n,o()),f=o=>({...o,testStats:o.statsSummary?.stats.perTestStats}),g=e.map(f),c=a?.map(o=>({key:o.key,items:o.items.map(f)})),m=at({title:"Test value distribution",axisPrefs:H,onAxisPrefsChange:u(()=>m),onTestChange:o=>y(o),selectedTestNumber:E??void 0,items:g,testDefs:t,groups:c,groupLabelText:s,onSaveImage:T,onGroupChange:o=>Q(o),onOpen:i?.open,openActionLabel:i?.label,onWaferContextMenu:X(e),ownerDocument:p}),h=it({title:"Value histogram",axisPrefs:H,onAxisPrefsChange:u(()=>h),onTestChange:o=>y(o),selectedTestNumber:E??void 0,items:e,testDefs:t,groups:a,onGroupChange:o=>Q(o),onSaveImage:T,ownerDocument:p}),b=ot({title:"Wafer-to-wafer trend",axisPrefs:H,onAxisPrefsChange:u(()=>b),onTestChange:o=>y(o),selectedTestNumber:E??void 0,items:e.map(o=>({label:o.label,key:o.waferIndex,dies:o.dies,testStats:o.statsSummary?.stats.perTestStats})),testDefs:t,onSaveImage:T,centreLabel:Xe(Ke(e.map(o=>o.wafer.metadata)),"mean"),onOpen:F?(o,n)=>K(o,`Wafer ${e.find(v=>v.waferIndex===o)?.label??o}`,n):void 0,onWaferContextMenu:X(e),ownerDocument:p}),y=o=>{E=o,m.setTest(o),h.setTest(o),b.setTest(o),S.setTest(o)};q.set("distribution",[m,h,b]);const S=nt({title:"Process capability",items:e,testDefs:t,onSaveImage:T,selectedTestNumber:E??void 0,onSelectTest:o=>y(o),ownerDocument:p});return r.append(S.card,m.card,h.card,b.card),{card:r,destroy:()=>{S.destroy(),m.destroy(),h.destroy(),b.destroy()}}}function je(e){const t=_(p),a=de(e).defs,s=e.flatMap(f=>f.dies),r=x.sweeps??[],i=r.filter(f=>!rt(f,a));i.length>0&&t.appendChild(Me(i,r.length));const u=r.map(f=>st({spec:f,dies:s,testDefs:a,onSaveImage:T,ownerDocument:p}));for(const f of u)t.appendChild(f.card);return{card:t,destroy:()=>{for(const f of u)f.destroy()}}}function Me(e,t){const a=p.createElement("div");a.setAttribute("role","status"),Object.assign(a.style,{gridColumn:"1 / -1",display:"flex",flexWrap:"wrap",alignItems:"center",gap:w.md,padding:`${w.sm} ${w.md}`,background:k.warnBg,border:`1px solid ${k.warnBorder}`,borderRadius:me.container,color:k.warnText,fontSize:A.body,lineHeight:fe.base});const s=e.length,r=p.createElement("span");if(r.style.flex="1 1 24ch",r.textContent=(s===t?s===1?"This sweep names no test in this data":"None of these sweeps names a test in this data":`${s} of ${t} sweeps name no test in this data: ${e.map(i=>i.title).join(", ")}`)+` \u2014 ${s===1?"it":"they"} may belong to another test program.`,a.appendChild(r),x.onRemoveSweeps){const i=p.createElement("button");i.type="button",Object.assign(i.style,be("outlined")),i.textContent=s===1?"Remove this sweep":`Remove these ${s} sweeps`,ne(i);const u=e.map(f=>f.id);i.addEventListener("click",()=>x.onRemoveSweeps(u)),a.appendChild(i)}return a}function Ye(e,t,a){const s=_(p),r=dt({title:"Test scatter",items:e,testDefs:t,groups:a,binColors:ae().hard,onSaveImage:T,axisPrefs:H,onAxisPrefsChange:u=>re(u,r),ownerDocument:p});q.set("correlation",[r]);const i=lt({title:"Test correlation matrix",items:e,testDefs:t,onSaveImage:T,onSaveText:U,onSelectPair:(u,f)=>r.setXY(u,f),ownerDocument:p});return s.append(i.card,r.card),{card:s,destroy:()=>{i.destroy(),r.destroy()}}}function V(e){e?.keepSections||ie(),G.innerHTML="",$.innerHTML="",x.backTab&&$.appendChild(Le(x.backTab));for(const n of De)$.appendChild(Ee(n.key,n.label));const t=$e();Ie(t);const a=He(t,{facetableOnly:!0}).filter(n=>n.splittable),s=p.createElement("div");Object.assign(s.style,{display:"flex",gap:w.md,alignItems:"center",marginLeft:w.xxl,paddingBottom:w.xs});let r;a.length>0?(s.appendChild(mt("Group by:",[{value:"",label:"None"},...a.map(n=>({value:n.key,label:`${n.label} (${n.values.length})`}))],j??"",n=>{j=n||void 0,L=null,V()},{hook:"group-by",ownerDocument:p})),r=a.find(n=>n.key===j)?.label):j=void 0;let i;if(j){const n=new Map,v=[];for(const l of t){const C=Fe(l.metadata,j)??Ne;n.has(C)||(n.set(C,[]),v.push(C)),n.get(C).push(l)}i=v.map(l=>({key:l,items:n.get(l)}))}L!==null&&!i?.some(n=>n.key===L)&&(L=null),i&&i.length>0&&s.appendChild(bt(i.map(n=>n.key),"Show:",L,n=>Q(n),{ownerDocument:p}).el);let u=null;if(x.onOpenGuide){const n=p.createElement("button");n.type="button",n.innerHTML=tt.help,n.setAttribute("aria-label","User guide"),Object.assign(n.style,{...be("bare"),display:"inline-flex",alignItems:"center",justifyContent:"center",width:"24px",height:"24px",flexShrink:"0"}),ne(n,"bare"),et(n,"User guide"),n.addEventListener("click",()=>x.onOpenGuide()),Object.assign(n.style,{marginLeft:"auto"}),u=n}$.appendChild(s),u&&$.appendChild(u);const f=L!==null?i?.find(n=>n.key===L):void 0,g=f?.items??t,c=f?void 0:i,m=de(g),h=m.defs,b=h.filter(Ve);E!==null&&!b.some(n=>n.testNumber===E)&&(E=null);const y=e?.keepSections?N.get(P):void 0,S=y??(P==="overview"?Re(g,b,h,c,r):P==="distributions"?We(g,b,c,r):P==="sweeps"?je(g):Ye(g,b,c));y||N.set(P,S);const o=m.conflicts.filter(n=>n.excluded);if(o.length>0){const n=p.createElement("div");Object.assign(n.style,{color:k.label,fontSize:A.body,padding:`0 ${Y} ${w.sm}`});const v=o.slice(0,6).map(O=>O.testNumber).join(", "),l=o.length>6?`, and ${o.length-6} more`:"",C=o.length===1?"it":"them";n.textContent=`${o.length} test ${o.length===1?"number is":"numbers are"} hidden (${v}${l}) \u2014 the wafers in view describe ${C} differently, so their values cannot be pooled. `+(i&&i.length>0?`Pick one ${r??"group"} under "Show:" to analyse ${C}.`:"Load one test program at a time to analyse them."),n.dataset.wmapWithheldTests=String(o.length),G.appendChild(n)}G.appendChild(S.card)}return{el:R,render:V,destroy:()=>{ie(),R.remove()}}}
|
|
1
|
+
import{MEAN_WAFER_YIELD_LABEL as Ue}from"../stats/presentation.js";import{buildFacetTable as qe,facetValueOf as Ke,FACET_NONE_VALUE as Qe}from"../stats/facets.js";import{mergeTestDefs as he}from"../stats/mergeTestDefs.js";import{isParametricTest as we}from"../renderer/buildWaferMap.js";import{dieFailsTest as Xe}from"../stats/testPassRate.js";import{fmt as Ce}from"../renderer/fmt.js";import{testLabel as Je}from"../renderer/testLabel.js";import{waferDisplayLabel as Ze,waferIdentityLabel as et}from"../core/waferLabel.js";import{waferPopulation as tt}from"./chartPopulation.js";import{openDrilldownMenu as se}from"./drilldown.js";import{plotStoreFor as nt}from"./plotStore.js";import{itemPassBins as ot,passBinsLabel as Se}from"../core/passBins.js";import{NO_DATA_FILL as xe}from"../renderer/colorMap.js";import{describeWaferPopulation as at,populationStat as rt}from"../stats/population.js";import{LEADING as it,ALPHA as st,SPACE as x,EDGE_GUTTER as le,FONT as M,CLR as B,RADIUS as lt,SHADOW as dt,controlStyle as ct,wireControlHover as de,wireTooltip as pt}from"./toolbar.js";import{ICONS as ut}from"./icons.js";import{renderCapabilityPanel as ft}from"./charts/capability.js";import{renderBoxplotPanel as mt}from"./charts/boxplot.js";import{renderTrendPanel as bt}from"./charts/trend.js";import{renderHistogramPanel as gt}from"./charts/histogram.js";import{renderCorrelationPanel as yt}from"./charts/correlation.js";import{renderScatterPanel as ht}from"./charts/scatter.js";import{renderBarPanel as ke}from"./charts/barPanel.js";import{renderBinClusterPanel as wt}from"./charts/binCluster.js";import{renderTestPassRatePanel as Ct}from"./charts/testPassRate.js";import{QUANTITY as St}from"./charts/palette.js";import{cardFrameStyle as xt,makeChartGridWrap as ce,makeLabeledSelect as kt,makeLinkedGroupSelect as vt}from"./charts/chartShell.js";import{buildYieldData as J,buildYieldDataCombined as ve}from"../stats/yield.js";import{buildBinParetoData as Tt}from"../stats/binPareto.js";import{buildLotTestSectionSteps as Dt,buildLotFunctionalSection as Bt,buildMetadataStripBox as $t}from"./summaryPanel.js";import{runChunked as Lt}from"./chunked.js";import{buildRegionYieldData as Te,buildRingRegions as De,buildQuadrantRegions as Be,diesInRegion as It}from"../stats/regions.js";import{renderDataSection as Et}from"./dataTab.js";import{sourceFromDies as Pt}from"./plotItems.js";import{renderRegionYieldDiagram as $e}from"./charts/regionYieldDiagram.js";const Ot=[{key:"overview",label:"Overview"},{key:"distributions",label:"Distributions"},{key:"correlation",label:"Correlation"}],At={key:"data",label:"Data"},Rt={key:"plot",label:"Plot"};export function createInsightsTab($){const{getItems:Le,getLotStats:pe,getBinColors:ue,getRingCount:F,onSaveImage:k,onSaveText:A,openWafer:R,focusTest:Z,locateDie:_}=$,Ie=$.showMetadataStrip??!0,z=$.contentInset??le,m=$.ownerDocument??document,N=m.createElement("div");Object.assign(N.style,{display:"none",flexDirection:"column",width:"100%",paddingTop:"0"});const Ee=le,Y=m.createElement("div");Object.assign(Y.style,{paddingLeft:Ee});const E=m.createElement("div");Object.assign(E.style,{display:"flex",gap:x.xs,alignItems:"center",background:B.menuBg,border:`1px solid ${B.menuBorder}`,borderRadius:lt.container,boxShadow:dt.panel,padding:`0 ${x.lg}`,marginBottom:x.lg,marginLeft:z,marginRight:z}),E.setAttribute("role","tablist"),E.addEventListener("keydown",e=>{if(e.key!=="ArrowLeft"&&e.key!=="ArrowRight")return;const n=Array.from(E.querySelectorAll('[role="tab"]')),a=n.indexOf(m.activeElement);if(a===-1)return;e.preventDefault();const d=n[(a+(e.key==="ArrowRight"?1:-1)+n.length)%n.length];d.focus(),d.click()});const U=m.createElement("div");Object.assign(U.style,{display:"flex",flexDirection:"column",gap:x.lg,marginTop:"0",paddingLeft:z,paddingRight:z}),N.appendChild(Y),N.appendChild(E),N.appendChild(U);const Pe=[...Ot,At,Rt],q=$.plotStore??nt($);let L=$.defaultView==="sweeps"?"plot":$.defaultView??"overview",W,fe="statistics",me="wide",P=null,O=null,K={includeLimits:void 0,clipOutliers:!1,limits:"both"};const ee=new Map,be=(e,n)=>{K=e;for(const a of ee.values())for(const d of a)d!==n&&d.setAxisPrefs(e)},te=e=>{O!==e&&(O=e,G())};let j=new Map;function ge(){for(const e of j.values())e.destroy();j=new Map}function ne(e,n,a){R?.(e,n,a)}function oe(e){return(n,a,d)=>{const r=e.find(p=>p.waferIndex===n);if(!r)return;d.preventDefault();const f=tt(r.dies,{waferLabel:r.identity,testDefs:r.testDefs,activeTest:a??P??void 0,waferIndex:n,wafer:r.wafer,passBins:r.passBins,ringCount:F()});se({x:d.clientX,y:d.clientY},d.target,f,{plots:q,onSaveImage:k,onSaveText:A,onLocateDie:_})}}function Q(e,n,a,d,r){ye({x:e.clientX,y:e.clientY},e.target,n,a,d,r)}function ye(e,n,a,d,r,f){const p=Pt(a.map(l=>({label:l.identity??l.label,dies:l.dies,waferIndex:l.waferIndex,wafer:l.wafer,passBins:l.passBins,ringCount:F()})),d,r,he(a).defs,f);p&&se(e,n,p,{plots:q,onSaveImage:k,onSaveText:A,onLocateDie:_})}function ae(e){return R?{open:(n,a)=>ne(n,`Wafer ${e.find(d=>d.waferIndex===n)?.label??n}`,a)}:Z&&e.length===1?{open:(n,a)=>Z(a),label:"show this test on the map"}:null}function Oe(){return Le().map((e,n)=>e==null?null:{metadata:e.wafer.metadata??void 0,dies:e.dies,wafer:e.wafer,label:Ze(e,n),identity:et(e),waferIndex:n,passBins:ot(e),statsSummary:e.statsSummary,testDefs:e.testDefs}).filter(e=>e!=null)}function Ae(e,n){Object.assign(e.style,{background:"none",border:"none",borderBottom:n?`2px solid ${B.iconActive}`:"2px solid transparent",color:n?B.iconActive:B.label,fontWeight:n?"700":"500",fontSize:M.body,padding:`${x.sm} ${le} ${x.md}`,cursor:"pointer",marginBottom:"-1px"})}function Re(e,n){const a=m.createElement("button");a.type="button",a.textContent=n;const d=e===L;return Ae(a,d),a.dataset.on=d?"true":"false",de(a,"bare"),a.dataset.wmapInsightsTab=e,a.setAttribute("role","tab"),a.setAttribute("aria-selected",d?"true":"false"),a.tabIndex=d?0:-1,a.addEventListener("click",()=>{L!==e&&(L=e,G({keepSections:!0}))}),a}function We(e,n,a,d=!0){const r=ce(m),f=ue();let p="label";const l=a??"group",y=Se(e.map(u=>u.passBins)),c=pe?.(),b=new Map(c?.lotYieldSeries.map(u=>[u.waferIndex,u.yieldPercent])),s=u=>({...u,label:u.label.replace(/\s*·\s*\d+(\.\d+)?%$/,""),yieldPercent:b.get(u.waferIndex),key:u.waferIndex}),g=e.map(s),h=n?.map(u=>({key:u.key,items:u.items.map(s)})),i=()=>h?ve(h,p):J(g,p),t={title:n?`Yield by ${l} (pass: ${y})`:`Yield by wafer (pass: ${y})`,data:i(),selfControl:{current:p,options:[["yield","Sort: yield"],["label",n?`Sort: ${l}`:"Sort: wafer ID"]],onChange:u=>(p=u,{data:i()})},barColor:()=>St,valueLabel:u=>`${u.percent.toFixed(1)}%`,reference:u=>{if(u.length<3)return null;const C=u.map(I=>I.percent).sort((I,V)=>I-V),D=C.length%2?C[(C.length-1)/2]:(C[C.length/2-1]+C[C.length/2])/2;return{value:D,label:`median ${D.toFixed(1)}%`}},drill:h?{onOpenGroup:u=>{const C=h.find(D=>D.key===u.label)?.items??[];return{data:J(C,p),title:`Yield by wafer \u2014 ${l}: ${u.label} (pass: ${y})`}},onBack:()=>({data:i(),title:`Yield by ${l} (pass: ${y})`}),groupLabelText:l}:void 0,onOpen:R?u=>{typeof u.key=="number"&&ne(u.key,`Wafer ${u.label}`)}:void 0,onWaferContextMenu:oe(e),ownerDocument:m};let o=null;if(d&&(o=ke(t,k),o.card.style.minHeight="360px",r.appendChild(o.card)),n){const u=wt({title:"Hard bin pareto",groups:n.map(C=>({key:C.key,items:C.items.map(D=>({dies:D.dies,hardBinCounts:D.statsSummary?.stats.hardBinCounts,softBinCounts:D.statsSummary?.stats.softBinCounts}))})),onSelectBin:(C,D)=>{const I=n.find(X=>X.key===C.groupKey);if(!I)return;const V=C.binType==="hbin"?"hard":"soft";Q(D,I.items,`in ${V} bin ${C.bin} of ${C.groupKey}`,X=>(C.binType==="hbin"?X.hbin:X.sbin)===C.bin)},onSaveImage:k,ownerDocument:m});return u.card.style.minHeight="360px",r.appendChild(u.card),{card:r,destroy:()=>{o?.destroy(),u.destroy()}}}let w="hbin";const S=u=>u===void 0?xe:(w==="hbin"?f.hard:f.soft).get(u)??xe,v=e.map(u=>({dies:u.dies,hardBinCounts:u.statsSummary?.stats.hardBinCounts,softBinCounts:u.statsSummary?.stats.softBinCounts})),H=()=>Tt(v,w),ie={title:"Hard bin pareto",data:H(),selfControl:{current:w,options:[["hbin","Hard bins"],["sbin","Soft bins"]],onChange:u=>(w=u,{data:H(),title:`${w==="hbin"?"Hard":"Soft"} bin pareto`})},barColor:u=>S(u.binCode),onSelectBar:(u,C)=>{const D=w==="hbin"?"hard":"soft",I=u.binCode;Q(C,e,I===void 0?`with no ${D} bin recorded`:`in ${D} bin ${I}`,V=>(w==="hbin"?V.hbin:V.sbin)===I)},selectLabel:"chart or tabulate its dies",ownerDocument:m},T=ke(ie,k);return T.card.style.minHeight="360px",r.appendChild(T.card),{card:r,destroy:()=>{o?.destroy(),T.destroy()}}}function re(){const e=m.createElement("div");return e.dataset.wmapChartCard="1",Object.assign(e.style,{...xt(),display:"flex",flexDirection:"column",gap:x.lg,minWidth:"0"}),e}function Me(e){if(Y.innerHTML="",Y.style.marginBottom="0",!Ie)return;const n=$t(e.map(a=>({metadata:a.metadata})),void 0,{facetableOnly:!1});n&&(n.style.marginBottom="0",Y.appendChild(n),Y.style.marginBottom=x.xs)}function Fe(e,n,a,d){if(!e.length)return{elements:[],destroy:()=>{}};const r=e.map(i=>i.wafer),f=e.map(i=>i.dies),p=F(),l=[],y=[],b=Ct({groups:(d??[{key:"",items:e}]).map(i=>({key:i.key,items:i.items.map(t=>({dies:t.dies,testSpecYield:t.statsSummary?.stats.testSpecYield,functionalYield:t.statsSummary?.stats.functionalYield}))})),testDefs:a,onSaveImage:k,onSelectTest:(i,t)=>{const o=a.find(w=>w.testNumber===i.testNumber);o&&Q(t,e,`failing ${i.label}`,w=>Xe(w,o,i.kind)===!0,we(o)?i.testNumber:void 0)},ownerDocument:m});b.card.style.minHeight="300px",l.push(b.card),y.push(b.destroy);const s=i=>(t,o)=>{const w=new Set;for(const S of e)for(const v of It(S.dies,S.wafer,p,t.key,i))w.add(v);Q(o,e,`in ${t.label}`,S=>w.has(S))},g=Te(f,r,p,i=>e[i].passBins,De);if(g.length){const i=$e({title:"Ring yield",mode:"ring",rows:g,onSelectRegion:s(De),onSaveImage:k,ownerDocument:m});l.push(i.card),y.push(i.destroy)}const h=Te(f,r,p,i=>e[i].passBins,Be);if(h.length){const i=$e({title:"Quadrant yield",mode:"quadrant",rows:h,onSelectRegion:s(Be),onSaveImage:k,ownerDocument:m});l.push(i.card),y.push(i.destroy)}return{elements:l,destroy:()=>{for(const i of y)i()}}}function Ne(e,n,a){if(!e.length)return{cards:[],destroy:()=>{}};const d=e.map(s=>s.dies),r=e.flatMap(s=>s.dies),f=e.map(s=>s.statsSummary),p=f.every(s=>s!==void 0)?f:void 0,l=[],y=[];let c=null;if(n.length){const s=re();let g=null;const h=Lt(Dt(r,n,void 0,p,A,d.map(i=>({dies:i})),void 0,"full"),{onDone:i=>{g=i,s.removeAttribute("aria-busy"),i?s.replaceChildren(i):s.remove()}});if(h.done)g&&(c=s);else{const i=m.createElement("div");i.textContent="Computing test statistics\u2026",Object.assign(i.style,{color:B.label,fontSize:M.body}),s.setAttribute("aria-busy","true"),s.appendChild(i),c=s,l.push(()=>h.cancel())}}let b=null;if(a.length){const s=Bt(r,a,p,A);if(s){const g=re();g.appendChild(s),b=g}}return c&&y.push(c),b&&y.push(b),{cards:y,destroy:()=>{for(const s of l)s()}}}function Ye(e){const n=Se(e.map(c=>c.passBins)),a=e.length===1,d=e[0],r=re();Object.assign(r.style,{flexDirection:"row",flexWrap:"wrap",gap:x.md});const f=c=>{const b=c.lastElementChild;return b&&Object.assign(b.style,{borderRight:"none",paddingRight:"0",marginRight:"0"}),c};function p(c,b,s){const g=m.createElement("div");Object.assign(g.style,{display:"flex",alignItems:"baseline",gap:x.sm,paddingRight:x.xxl,marginRight:x.md,borderRight:`1px solid ${B.menuBorder}`});const h=m.createElement("div");h.textContent=c,Object.assign(h.style,{fontSize:M.heading,fontWeight:"700",color:B.value,lineHeight:it.tight});const i=m.createElement("div");if(i.textContent=b,Object.assign(i.style,{fontSize:M.body,color:B.label}),g.append(h,i),s){const t=m.createElement("div");t.textContent=s,Object.assign(t.style,{fontSize:M.meta,color:B.label,opacity:st.muted}),g.appendChild(t)}return g}if(a){const c=J([{...d,key:d.waferIndex}])[0]?.percent;return c!==void 0&&r.appendChild(p(`${c.toFixed(1)}%`,`Yield \xB7 pass: ${n}`)),r.appendChild(p(String(d.dies.length),"Total dies")),f(r)}r.appendChild(p(String(e.length),"Wafers"));const l=J(e.map(c=>({...c,key:c.waferIndex}))).map(c=>c.percent).filter(c=>Number.isFinite(c));if(l.length){const c=l.reduce((g,h)=>g+h,0)/l.length,b=ve([{key:"all",items:e.map(g=>({...g,key:g.waferIndex}))}])[0]?.percent,s=b!==void 0&&b.toFixed(1)!==c.toFixed(1);r.appendChild(p(`${c.toFixed(1)}%`,`${Ue} \xB7 pass: ${n}`,s?`${b.toFixed(1)}% by dies`:void 0))}const y=e.map(c=>c.statsSummary);if(y.every(c=>c!==void 0)){const c=y.reduce((s,g)=>s+g.stats.analyzedDies,0),b=y.reduce((s,g)=>s+g.stats.excludedDies,0);r.appendChild(p(c.toLocaleString(),"Dies analysed",b?`${b.toLocaleString()} excluded`:void 0))}else r.appendChild(p(e.reduce((c,b)=>c+b.dies.length,0).toLocaleString(),"Total dies"));return f(r)}function je(e,n,a,d,r){const f=m.createElement("div");Object.assign(f.style,{display:"flex",flexDirection:"column",gap:x.lg});const p=e.length===1&&!d;e.length&&!d&&f.appendChild(Ye(e));const l=We(e,d,r,!p);f.appendChild(l.card);const y=Fe(e,n,a,d);for(const c of y.elements)l.card.appendChild(c);return f.appendChild(Ge()),{card:f,destroy:()=>{l.destroy(),y.destroy()}}}function Ge(){const e=m.createElement("div");Object.assign(e.style,{fontSize:M.body,color:B.label});const n=m.createElement("button");return n.type="button",n.textContent="Data tab",n.dataset.wmapOpenDataTab="1",Object.assign(n.style,{border:"none",background:"none",padding:"0",cursor:"pointer",font:"inherit",color:B.iconActive,textDecoration:"underline"}),de(n,"bare"),n.addEventListener("click",()=>{L="data",G({keepSections:!0})}),e.append("Test statistics, every die and every wafer as tables, with CSV export: see the ",n,"."),e}function He(e,n,a,d){const r=m.createElement("div");r.style.width="100%";let f,p=!1;return import("./plotTab.js").then(({createPlotSection:l})=>{if(p)return;const y=l({doc:m,store:q,items:e,testDefs:n,ringCount:F(),groupBy:a,groupLabel:d,onSaveImage:k,onSaveText:A,openWafer:R?(c,b,s)=>R(c,b,s):void 0,focusTest:Z,locateDie:_,pickPlotsFile:$.onPickPlotsFile});r.appendChild(y.card),f=y}),{card:r,destroy:()=>{p=!0,f?.destroy()}}}function Ve(e,n,a,d,r){const f=pe?.(),p=()=>{j.get("data")?.destroy(),j.delete("data"),G({keepSections:!0})};return Et({doc:m,items:e,testDefs:n,allTestDefs:a,groups:d,groupLabelText:r,ringCount:F(),yieldByWaferIndex:new Map(f?.lotYieldSeries.map(l=>[l.waferIndex,l.yieldPercent])),onSaveText:A,onLocateDie:_,onOpenWafer:R?(l,y)=>R(l,y):void 0,buildStatistics:l=>Ne(l,n,a),view:fe,onViewChange:l=>{fe=l,p()},diesLayout:me,onDiesLayoutChange:l=>{me=l}})}function _e(e,n,a,d){const r=ce(m),f=ae(e),p=t=>o=>be(o,t()),l=t=>({...t,testStats:t.statsSummary?.stats.perTestStats}),y=e.map(l),c=a?.map(t=>({key:t.key,items:t.items.map(l)})),b=mt({title:"Test value distribution",axisPrefs:K,onAxisPrefsChange:p(()=>b),onTestChange:t=>h(t),selectedTestNumber:P??void 0,items:y,testDefs:n,groups:c,groupLabelText:d,onSaveImage:k,onGroupChange:t=>te(t),onOpen:f?.open,openActionLabel:f?.label,onWaferContextMenu:oe(e),ownerDocument:m}),s=gt({title:"Value histogram",axisPrefs:K,onAxisPrefsChange:p(()=>s),onTestChange:t=>h(t),selectedTestNumber:P??void 0,items:e,testDefs:n,groups:a,onGroupChange:t=>te(t),onSaveImage:k,onSelectBucket:(t,o)=>{const w=n.find(T=>T.testNumber===t.testNumber),S=new Set(t.items.map(T=>T.item)),v=new Set(t.items.flatMap(T=>T.dies)),H=Ce(t.low,w?.unit,"engineering"),ie=Ce(t.high,w?.unit,"engineering");Q(o,e.filter(T=>S.has(T)),`with ${w?Je(w,w.testNumber):`test ${t.testNumber}`} from ${H} to ${ie}${t.group?` in ${t.group}`:""}`,T=>v.has(T),t.testNumber)},ownerDocument:m}),g=bt({title:"Wafer-to-wafer trend",axisPrefs:K,onAxisPrefsChange:p(()=>g),onTestChange:t=>h(t),selectedTestNumber:P??void 0,items:e.map(t=>({label:t.label,key:t.waferIndex,dies:t.dies,testStats:t.statsSummary?.stats.perTestStats})),testDefs:n,onSaveImage:k,centreLabel:rt(at(e.map(t=>t.wafer.metadata)),"mean"),onOpen:R?(t,o)=>ne(t,`Wafer ${e.find(w=>w.waferIndex===t)?.label??t}`,o):void 0,onSelectWafers:(t,o,w)=>{const S=new Set(t.keys);ye(o,w,e.filter(v=>S.has(v.waferIndex)),"selected in the trend",()=>!0,t.testNumber)},onWaferContextMenu:oe(e),ownerDocument:m}),h=t=>{P=t,b.setTest(t),s.setTest(t),g.setTest(t),i.setTest(t)};ee.set("distribution",[b,s,g]);const i=ft({title:"Process capability",items:e,testDefs:n,onSaveImage:k,selectedTestNumber:P??void 0,onSelectTest:t=>h(t),ownerDocument:m});return r.append(i.card,b.card,s.card,g.card),{card:r,destroy:()=>{i.destroy(),b.destroy(),s.destroy(),g.destroy()}}}function ze(e,n,a){const d=ce(m),r=ht({title:"Test scatter",items:e,testDefs:n,groups:a,binColors:ue().hard,onSaveImage:k,axisPrefs:K,onAxisPrefsChange:p=>be(p,r),onOpen:ae(e)?.open,openActionLabel:ae(e)?.label,onSelect:(p,l,y,c)=>{const b=new Map;for(const h of p){if(h.waferIndex===void 0||!h.die)continue;const i=b.get(h.waferIndex);i?i.push(h.die):b.set(h.waferIndex,[h.die])}const s=[...b].flatMap(([h,i])=>{const t=e.find(o=>o.waferIndex===h);return t?[{label:t.identity??t.label,dies:i,waferIndex:h,wafer:t.wafer,passBins:t.passBins,ringCount:F()}]:[]});if(s.length===0)return;const g=s.length===1?`selected on ${s[0].label} in the scatter`:`selected in the scatter, across ${s.length} wafers`;se(l,y,{items:s,population:g,testDefs:n,activeTest:c},{plots:q,onSaveImage:k,onSaveText:A,onLocateDie:_})},ownerDocument:m});ee.set("correlation",[r]);const f=yt({title:"Test correlation matrix",items:e,testDefs:n,onSaveImage:k,onSaveText:A,onSelectPair:(p,l)=>r.setXY(p,l),ownerDocument:m});return d.append(f.card,r.card),{card:d,destroy:()=>{f.destroy(),r.destroy()}}}function G(e){e?.keepSections||ge(),U.innerHTML="",E.innerHTML="";for(const o of Pe)E.appendChild(Re(o.key,o.label));const n=Oe();Me(n);const a=qe(n,{facetableOnly:!0}).filter(o=>o.splittable),d=m.createElement("div");Object.assign(d.style,{display:"flex",gap:x.md,alignItems:"center",marginLeft:x.xxl,paddingBottom:x.xs});let r;a.length>0?(d.appendChild(kt("Group by:",[{value:"",label:"None"},...a.map(o=>({value:o.key,label:`${o.label} (${o.values.length})`}))],W??"",o=>{W=o||void 0,O=null,G()},{hook:"group-by",ownerDocument:m})),r=a.find(o=>o.key===W)?.label):W=void 0;let f;if(W){const o=new Map,w=[];for(const S of n){const v=Ke(S.metadata,W)??Qe;o.has(v)||(o.set(v,[]),w.push(v)),o.get(v).push(S)}f=w.map(S=>({key:S,items:o.get(S)}))}O!==null&&!f?.some(o=>o.key===O)&&(O=null),f&&f.length>0&&d.appendChild(vt(f.map(o=>o.key),"Show:",O,o=>te(o),{ownerDocument:m}).el);let p=null;if($.onOpenGuide){const o=m.createElement("button");o.type="button",o.innerHTML=ut.help,o.setAttribute("aria-label","User guide"),Object.assign(o.style,{...ct("bare"),display:"inline-flex",alignItems:"center",justifyContent:"center",width:"24px",height:"24px",flexShrink:"0"}),de(o,"bare"),pt(o,"User guide"),o.addEventListener("click",()=>$.onOpenGuide()),Object.assign(o.style,{marginLeft:"auto"}),p=o}E.appendChild(d),p&&E.appendChild(p);const l=O!==null?f?.find(o=>o.key===O):void 0,y=l?.items??n,c=l?void 0:f,b=he(y),s=b.defs,g=s.filter(we);P!==null&&!g.some(o=>o.testNumber===P)&&(P=null);const h=e?.keepSections?j.get(L):void 0,i=h??(L==="overview"?je(y,g,s,c,r):L==="distributions"?_e(y,g,c,r):L==="data"?Ve(y,g,s,c,r):L==="plot"?He(y,s,l?void 0:W,l?void 0:r):ze(y,g,c));h||j.set(L,i);const t=b.conflicts.filter(o=>o.excluded);if(t.length>0){const o=m.createElement("div");Object.assign(o.style,{color:B.label,fontSize:M.body,padding:`0 ${z} ${x.sm}`});const w=t.slice(0,6).map(H=>H.testNumber).join(", "),S=t.length>6?`, and ${t.length-6} more`:"",v=t.length===1?"it":"them";o.textContent=`${t.length} test ${t.length===1?"number is":"numbers are"} hidden (${w}${S}) \u2014 the wafers in view describe ${v} differently, so their values cannot be pooled. `+(f&&f.length>0?`Pick one ${r??"group"} under "Show:" to analyse ${v}.`:"Load one test program at a time to analyse them."),o.dataset.wmapWithheldTests=String(t.length),U.appendChild(o)}U.appendChild(i.card)}return{el:N,render:G,destroy:()=>{ge(),q.flush(),N.remove()}}}
|
|
@@ -7,8 +7,8 @@ export interface MaplessSummaryOptions {
|
|
|
7
7
|
activeTest?: number;
|
|
8
8
|
hbinDefs?: BinDef[];
|
|
9
9
|
sbinDefs?: BinDef[];
|
|
10
|
-
/** The map's resolved bin colours (`View.binColors`), so bars match its legend. */
|
|
11
|
-
binColors
|
|
10
|
+
/** The map's resolved bin colours (`View.binColors`), so bars match its legend and judge by the map's own pass bins. Required: there is no default to resolve them by. */
|
|
11
|
+
binColors: BinColors;
|
|
12
12
|
/** The map's value gradient name, so histogram bars match its colorbar. */
|
|
13
13
|
valueColorScheme?: string;
|
|
14
14
|
/** The map's `View.reverseValueScheme` — must travel with the name, or the
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type FieldOption } from '../stats/plotData.js';
|
|
2
|
+
import { type PlotSpec } from '../stats/plotSpec.js';
|
|
3
|
+
export interface PlotEditorOptions {
|
|
4
|
+
doc: Document;
|
|
5
|
+
plot: PlotSpec;
|
|
6
|
+
catalogue: readonly FieldOption[];
|
|
7
|
+
/** What "Follow Group by" means right now ("Split", or "a colour per wafer" with no grouping), or undefined for a single colour. */
|
|
8
|
+
followLabel: () => string | undefined;
|
|
9
|
+
/** The automatic title and axis titles, shown as placeholders so the reader sees what clearing a box gives back. */
|
|
10
|
+
autoText: () => {
|
|
11
|
+
title: string;
|
|
12
|
+
x?: string;
|
|
13
|
+
y?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Called with the new spec after every change. */
|
|
16
|
+
onChange: (next: PlotSpec) => void;
|
|
17
|
+
}
|
|
18
|
+
export interface PlotEditorHandle {
|
|
19
|
+
el: HTMLElement;
|
|
20
|
+
/** The spec changed elsewhere (undo, a rebind): show it. */
|
|
21
|
+
setPlot(plot: PlotSpec): void;
|
|
22
|
+
/** The automatic text changed (the data did): refresh the placeholders. */
|
|
23
|
+
refreshAuto(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare function inputStyle(): Partial<CSSStyleDeclaration>;
|
|
26
|
+
/** A labelled control with an optional hint beneath it: the one layout every editor field uses. */
|
|
27
|
+
export declare function fieldRow(doc: Document, label: string, control: HTMLElement, hint?: string): HTMLElement;
|
|
28
|
+
export declare function createPlotEditor(o: PlotEditorOptions): PlotEditorHandle;
|
|
29
|
+
//# sourceMappingURL=plotEditor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{combinationIssue as le,describeTitleDrift as oe,fieldKey as k,fieldsForRole as H,titleDrift as ae}from"../stats/plotData.js";import{sameField as j}from"../stats/plotSpec.js";import{CLR as m,FONT as S,RADIUS as V,SPACE as b,controlStyle as K,wireControlHover as _}from"./toolbar.js";import{makeListSelect as X,makeSegmented as N,makeToggle as se}from"./charts/chartShell.js";const Y="\0follow",z="\0none",re=[["scatter","Scatter"],["histogram","Histogram"],["box","Box"],["bar","Bar"],["line","Line"]],A="\0auto",G="\0count",Q={mean:"Mean",median:"Median",min:"Minimum",max:"Maximum",sum:"Sum",count:"Count",yield:"Pooled yield (passing over judged dies)"};export function inputStyle(){return{background:m.menuBg,color:m.text,border:`1px solid ${m.menuBorder}`,borderRadius:V.control,padding:`${b.xs} ${b.sm}`,fontSize:S.body,fontFamily:S.family,boxSizing:"border-box",minWidth:"0"}}export function fieldRow(c,r,B,a){const p=c.createElement("div");Object.assign(p.style,{display:"flex",flexDirection:"column",gap:b.xxs,minWidth:"0"});const x=c.createElement("div");if(x.textContent=r,Object.assign(x.style,{fontSize:S.sub,color:m.label,fontWeight:"600"}),p.append(x,B),a){const T=c.createElement("div");T.textContent=a,Object.assign(T.style,{fontSize:S.sub,color:m.label}),p.appendChild(T)}return p}export function createPlotEditor(c){const{doc:r}=c,B=t=>t.fields?t:{...t,fields:{}};let a=B(c.plot),p="setup";const x=r.createElement("div");x.dataset.wmapPlotEditor="1",Object.assign(x.style,{display:"flex",flexDirection:"column",gap:b.lg,minWidth:"0"});const T=t=>{a=t,c.onChange(t)},y=t=>{const e=JSON.parse(JSON.stringify(a));t(e),T(e)},d=(t,e,i)=>fieldRow(r,t,e,i),D=t=>{const e=t&&c.catalogue.find(i=>j(i.field,t));return e?{label:e.name,level:e.level,kind:e.kind,yield:"builtin"in e.field&&e.field.builtin==="yield"}:void 0};function Z(t,e){const i=a.fields??{},n=i.color&&!("follow"in i.color)&&!("none"in i.color)?i.color:void 0,l={x:D(i.x),y:D(i.y),color:D(n)};l[t]=D(e.field);const o=a.chart==="scatter"&&l.color?.kind==="numeric"&&!!n&&!("meta"in n);return le(a.chart,l,{aggregate:a.aggregate,level:a.level,continuousColor:o})?.short}const I=t=>{const e=a.fields?.[t],i=e&&!("follow"in e)&&!("none"in e)?e:void 0;return H(c.catalogue,a.chart,t).map(n=>{const l=i&&j(n.field,i)?void 0:Z(t,n);return{value:k(n.field),label:n.label,group:n.group,...l?{disabled:l}:{}}})},J=t=>c.catalogue.find(e=>k(e.field)===t)?.field;function v(t,e,i={}){const n=a.fields[t],l=I(t);if(i.optional&&l.unshift({value:G,label:i.optional,group:"Count"}),n&&!l.some(u=>u.value===k(n))){const u=c.catalogue.some(C=>j(C.field,n)),h="test"in n?n.name??`Test ${n.test}`:"meta"in n?n.meta:n.builtin;l.unshift({value:k(n),label:`${h} (${u?"not usable here":"not in this lot"})`,group:"Saved"})}const o=n?k(n):i.optional?G:"",s=X(l,o,u=>{if(u===G){y(C=>{delete C.fields[t]});return}const h=J(u)??a.fields[t];h&&y(C=>{C.fields[t]=h})},{maxWidth:"100%",ownerDocument:r,ariaLabel:e,emptyText:"Choose a field\u2026",searchPlaceholder:"Filter fields\u2026",hook:`plot-${t}`});return s.style.width="100%",s}const P={},F={};function ee(t){y(e=>{e.chart=t;for(const i of["x","y"]){const n=H(c.catalogue,t,i),l=h=>!!h&&n.some(C=>j(C.field,h)),o=e.fields[i];if(o&&!l(o)&&(j(o,F[i])||(P[i]=o),delete e.fields[i]),e.fields[i]||i==="y"&&t==="bar")continue;const s=P[i];if(l(s)){e.fields[i]=s,delete P[i],delete F[i];continue}const u=i==="x"&&(t==="box"||t==="bar")?{builtin:"wafer"}:i==="x"&&t==="line"?n.find(h=>h.group==="Wafer")?.field??n[0]?.field:n[0]?.field;u&&!(i==="x"&&t==="histogram")&&(e.fields[i]=u,F[i]=u)}}),E()}function te(){const t=!!a.fields.y&&"builtin"in a.fields.y&&a.fields.y.builtin==="yield",e=[{value:A,label:"Automatic"},...["mean","median","min","max","sum","count"].map(n=>({value:n,label:Q[n]}))];t&&e.push({value:"yield",label:Q.yield});const i=X(e,a.aggregate??A,n=>y(l=>{n===A?delete l.aggregate:l.aggregate=n}),{maxWidth:"100%",ownerDocument:r,ariaLabel:"Combine values by",hook:"plot-aggregate"});return i.style.width="100%",i}function ie(){const t=a.fields.color,e=c.followLabel(),i=[{value:Y,label:e?`Follow Group by (${e})`:"Follow Group by (single colour)",group:"Colour"},{value:z,label:"None",group:"Colour"},...I("color")],n=t===void 0||"follow"in t?Y:"none"in t?z:k(t),l=X(i,n,o=>{y(s=>{if(o===Y)delete s.fields.color;else if(o===z)s.fields.color={none:!0};else{const u=J(o);u&&(s.fields.color=u)}})},{maxWidth:"100%",ownerDocument:r,ariaLabel:"Colour",searchPlaceholder:"Filter fields\u2026",hook:"plot-color"});return l.style.width="100%",l}function U(t,e,i,n){const l=r.createElement("input");return l.type="text",l.value=t,l.placeholder=e,l.setAttribute("aria-label",i),Object.assign(l.style,inputStyle(),{width:"100%"}),l.addEventListener("input",()=>n(l.value)),l.addEventListener("keydown",o=>{o.key==="Escape"&&(o.stopPropagation(),l.blur())}),l}function R(t,e,i){const n=r.createElement("input");n.type="text",n.inputMode="decimal",n.value=t===void 0?"":String(t),n.placeholder="auto",n.setAttribute("aria-label",e),Object.assign(n.style,inputStyle(),{width:"100%",fontVariantNumeric:"tabular-nums"});const l=()=>{const o=n.value.trim();if(o===""){i(void 0);return}const s=Number(o);Number.isFinite(s)?i(s):n.value=t===void 0?"":String(t)};return n.addEventListener("change",l),n.addEventListener("keydown",o=>{o.key==="Enter"&&l(),o.key==="Escape"&&(o.stopPropagation(),n.value=t===void 0?"":String(t),n.blur())}),n}function q(t,e,i,n){const l=a.axes?.[t]??{},o=g=>y(f=>{f.axes=f.axes??{};const O=f.axes[t]=f.axes[t]??{};g(O);for(const $ of Object.keys(O))(O[$]===void 0||O[$]===""||$==="reverse"&&O[$]===!1)&&delete O[$];Object.keys(O).length===0&&delete f.axes[t],Object.keys(f.axes).length===0&&delete f.axes}),s=r.createElement("div");Object.assign(s.style,{display:"flex",flexDirection:"column",gap:b.md,padding:b.lg,border:`1px solid ${m.menuBorder}`,borderRadius:V.control});const u=r.createElement("div");u.textContent=e,Object.assign(u.style,{fontSize:S.body,fontWeight:"600",color:m.value}),s.appendChild(u);const h=U(l.label??"",i??"",`${e} title`,g=>o(f=>{f.label=g.trim()===""?void 0:g}));if(h.dataset.wmapPlotAxisTitle=t,s.appendChild(d("Title",h)),n.limits===!1)return s;const C=r.createElement("div");return Object.assign(C.style,{display:"grid",gridTemplateColumns:"1fr 1fr",gap:b.md}),C.append(d("Minimum",R(l.min,`${e} minimum`,g=>o(f=>{f.min=g}))),d("Maximum",R(l.max,`${e} maximum`,g=>o(f=>{f.max=g})))),s.appendChild(C),n.scale&&s.appendChild(d("Scale",N([["linear","Linear"],["log","Log"]],l.scale??"linear",g=>o(f=>{f.scale=g==="log"?"log":void 0}),r))),s.appendChild(se("Reverse direction",!!l.reverse,g=>o(f=>{f.reverse=g}),r)),s}const ne=t=>x.querySelector(`[role="tab"][data-tab="${t}"]`);let L,w;const W=new Map;function M(){if(!w)return;const t=oe(ae(a,c.catalogue));if(w.replaceChildren(),w.style.display=t?"flex":"none",!t)return;const e=r.createElement("span");e.textContent=t;const i=r.createElement("button");i.type="button",i.textContent="Use the automatic title",i.dataset.wmapPlotAutoTitle="1",Object.assign(i.style,K("outlined")),_(i),i.addEventListener("click",()=>{y(n=>{delete n.title}),E()}),w.append(e,i)}function E(){x.replaceChildren(),W.clear();const t=r.createElement("div");t.setAttribute("role","tablist"),Object.assign(t.style,{display:"flex",gap:b.sm,borderBottom:`1px solid ${m.menuBorder}`});for(const[i,n]of[["setup","Setup"],["customise","Customise"]]){const l=r.createElement("button");l.type="button",l.textContent=n,l.setAttribute("role","tab"),l.dataset.tab=i,l.setAttribute("aria-selected",String(p===i)),Object.assign(l.style,{...K("bare"),padding:`${b.sm} ${b.lg}`,fontSize:S.body,borderBottom:`2px solid ${p===i?m.iconActive:"transparent"}`,borderRadius:"0",color:p===i?m.iconActive:m.text,fontWeight:p===i?"600":"400"}),_(l,"bare"),l.tabIndex=p===i?0:-1,l.addEventListener("click",()=>{p=i,E()}),l.addEventListener("keydown",o=>{o.key!=="ArrowLeft"&&o.key!=="ArrowRight"||(o.preventDefault(),p=i==="setup"?"customise":"setup",E(),ne(p)?.focus())}),t.appendChild(l)}x.appendChild(t);const e=r.createElement("div");if(e.setAttribute("role","tabpanel"),Object.assign(e.style,{display:"flex",flexDirection:"column",gap:b.lg}),p==="setup"){e.appendChild(d("Chart type",N(re,a.chart,n=>ee(n),r,!0)));const i=a.level==="wafer"?"wafers":"dies";switch(a.chart){case"scatter":e.appendChild(d("X axis",v("x","X axis"))),e.appendChild(d("Y axis",v("y","Y axis")));break;case"histogram":e.appendChild(d("Values",v("y","Values")));break;case"box":e.appendChild(d("Categories (X axis)",v("x","Categories"),"One box per value of this field.")),e.appendChild(d("Values (Y axis)",v("y","Values")));break;case"bar":e.appendChild(d("Categories (X axis)",v("x","Categories"),"One bar per value of this field.")),e.appendChild(d("Values (Y axis)",v("y","Values",{optional:`Count of ${i}`})));break;case"line":e.appendChild(d("X axis",v("x","X axis"),"A line joins the value at each X.")),e.appendChild(d("Y axis",v("y","Y axis")));break}e.appendChild(d("Colour",ie(),"Each value of the field gets its own colour. Follow Group by takes it from the Group by control.")),(a.chart==="bar"||a.chart==="line"||a.level==="wafer")&&e.appendChild(d("Combine values by",te())),e.appendChild(d("One mark per",N([[A,"Automatic"],["die","Die"],["wafer","Wafer"]],a.level??A,n=>{y(l=>{n===A?delete l.level:l.level=n}),E()},r,!0),a.level==="wafer"?"Die-level values are combined per wafer.":"Automatic uses the finest level the chosen fields allow."))}else{L=U(a.title??"",c.autoText().title,"Plot title",o=>{y(s=>{o.trim()===""?delete s.title:Object.assign(s,{title:o})}),M()}),L.dataset.wmapPlotTitle="1",w=r.createElement("div"),w.dataset.wmapPlotTitleDrift="1",Object.assign(w.style,{display:"none",flexDirection:"column",gap:b.sm,alignItems:"flex-start",padding:`${b.sm} ${b.md}`,background:m.warnBg,border:`1px solid ${m.warnBorder}`,borderRadius:V.control,color:m.warnText,fontSize:S.body}),e.appendChild(d("Title",L)),e.appendChild(w),M();const i=c.autoText(),n=a.chart==="box"||a.chart==="bar";e.appendChild(q("x",a.chart==="histogram"?"Values axis":n?"Categories axis":"X axis",i.x,{scale:!n,limits:!n})),e.appendChild(q("y",a.chart==="histogram"?"Count axis":n||a.chart==="line"?"Values axis":"Y axis",i.y,{scale:a.chart!=="histogram"})),a.chart==="histogram"&&e.appendChild(d("Bins",R(a.bins,"Number of bins",o=>y(s=>{o===void 0||!Number.isInteger(o)||o<1?delete s.bins:s.bins=Math.min(o,200)})),"Automatic is 16 equal bins."));const l=a.fields??{};(l.x&&"test"in l.x||l.y&&"test"in l.y)&&e.appendChild(d("Limits",N([["both","Test + spec"],["test","Test"],["spec","Spec"],["none","None"]],a.limits??"both",o=>y(s=>{o==="both"?delete s.limits:s.limits=o}),r),"Drawn as dashed lines on the axis of a test that has them: short dashes for test limits, long for spec limits."));for(const o of e.querySelectorAll("[data-wmap-plot-axis-title]"))W.set(o.dataset.wmapPlotAxisTitle,o)}x.appendChild(e)}return E(),{el:x,setPlot(t){const e=B(t);JSON.stringify(e)!==JSON.stringify(a)&&(a=e,E())},refreshAuto(){const t=c.autoText();L&&(L.placeholder=t.title),M();for(const[e,i]of W)i.placeholder=(e==="x"?t.x:t.y)??""}}}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Die } from '../core/dies.js';
|
|
2
|
+
import type { Wafer } from '../core/wafer.js';
|
|
3
|
+
import type { TestDef } from '../renderer/buildWaferMap.js';
|
|
4
|
+
import type { PlotItem, PlotPoint } from '../stats/plotData.js';
|
|
5
|
+
import { type DrilldownSource } from './chartPopulation.js';
|
|
6
|
+
/** What a population hands over for each wafer: a drilldown source's items, or the Insights tab's. */
|
|
7
|
+
export interface WaferShare {
|
|
8
|
+
label: string;
|
|
9
|
+
dies: readonly Die[];
|
|
10
|
+
waferIndex?: number;
|
|
11
|
+
wafer?: Wafer;
|
|
12
|
+
passBins: readonly number[];
|
|
13
|
+
ringCount: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function toPlotItems(items: readonly WaferShare[]): PlotItem[];
|
|
16
|
+
/**
|
|
17
|
+
* The dies a drag on a plot picked out, per wafer, as a drilldown population: charts and tables of just these.
|
|
18
|
+
* `null` when the drag caught no die (a wafer-level plot has none to pick).
|
|
19
|
+
*/
|
|
20
|
+
export declare function sourceFromPoints(points: readonly PlotPoint[], items: readonly PlotItem[], testDefs: readonly TestDef[] | undefined): DrilldownSource | null;
|
|
21
|
+
/**
|
|
22
|
+
* The dies of each wafer that satisfy `keep`, as a drilldown population worded "`what` on W03" or "`what`, across N
|
|
23
|
+
* wafers". `null` when none does. Per wafer, so the tables name each die's wafer and a chart opened on it is of
|
|
24
|
+
* exactly those dies.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sourceFromDies(shares: readonly WaferShare[], what: string, keep: (d: Die) => boolean, testDefs: readonly TestDef[] | undefined, activeTest?: number): DrilldownSource | null;
|
|
27
|
+
/**
|
|
28
|
+
* The dies in `bins` (hard or soft), per wafer, for a legend entry's right-click: the same population a bin pareto bar
|
|
29
|
+
* opens. `null` when no die is in them. A stack of a lot holds aggregates, not dies, so it is named as not measured.
|
|
30
|
+
*/
|
|
31
|
+
export declare function sourceFromBins(shares: readonly WaferShare[], kind: 'hard' | 'soft', bins: readonly number[], testDefs: readonly TestDef[] | undefined, activeTest?: number, stacked?: boolean): DrilldownSource | null;
|
|
32
|
+
//# sourceMappingURL=plotItems.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LOT_STACK_REASON as d}from"./chartPopulation.js";export function toPlotItems(l){return l.map(e=>({label:e.label,dies:e.dies,waferIndex:e.waferIndex,wafer:e.wafer,passBins:e.passBins,ringCount:e.ringCount,metadata:e.wafer?.metadata}))}export function sourceFromPoints(l,e,o){const s=new Map;for(const r of l){const n=s.get(r.item);if(!r.die){s.set(r.item,"all");continue}n!=="all"&&(n?n.push(r.die):s.set(r.item,[r.die]))}const t=[...s].map(([r,n])=>({label:e[r].label,dies:n==="all"?[...e[r].dies]:n,waferIndex:e[r].waferIndex,wafer:e[r].wafer,passBins:e[r].passBins,ringCount:e[r].ringCount}));if(t.length===0)return null;const a=t.length===1?`selected on ${t[0].label} in the plot`:`selected in the plot, across ${t.length} wafers`;return{items:t,population:a,testDefs:o?[...o]:void 0}}export function sourceFromDies(l,e,o,s,t){const a=l.map(n=>({it:n,dies:n.dies.filter(o)})).filter(n=>n.dies.length>0).map(({it:n,dies:f})=>({label:n.label,dies:f,waferIndex:n.waferIndex,wafer:n.wafer,passBins:n.passBins,ringCount:n.ringCount}));if(a.length===0)return null;const r=a.length===1?`${e} on ${a[0].label}`:`${e}, across ${a.length} wafers`;return{items:a,population:r,testDefs:s?[...s]:void 0,activeTest:t}}export function sourceFromBins(l,e,o,s,t,a=!1){const r=new Set(o),n=`in ${e} bin${o.length===1?"":"s"} ${o.join(", ")}`,f=sourceFromDies(l,n,i=>{const u=e==="hard"?i.hbin:i.sbin;return u!==void 0&&r.has(u)},s,t);return f&&a&&(f.notMeasuredReason=d),f}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PlotContext, PlotItem } from '../stats/plotData.js';
|
|
2
|
+
import { type PlotSpec } from '../stats/plotSpec.js';
|
|
3
|
+
import { type PlotChartOptions } from './charts/plotChart.js';
|
|
4
|
+
import { type SweepPanelOptions } from './charts/sweep.js';
|
|
5
|
+
import type { PlotStore } from './plotStore.js';
|
|
6
|
+
export interface PlotEditorWindowOptions {
|
|
7
|
+
doc: Document;
|
|
8
|
+
anchor: HTMLElement;
|
|
9
|
+
store: PlotStore;
|
|
10
|
+
plot: PlotSpec;
|
|
11
|
+
/** A draft is not in the store until added. */
|
|
12
|
+
draft: boolean;
|
|
13
|
+
items: PlotItem[];
|
|
14
|
+
ctx: PlotContext;
|
|
15
|
+
/** The label "Follow Group by" resolves to, when the tab has one. */
|
|
16
|
+
groupLabel?: string;
|
|
17
|
+
/** Pointer callbacks for the chart (open a wafer, drag-select). */
|
|
18
|
+
chart: Omit<PlotChartOptions, 'ownerDocument' | 'waferLabel'>;
|
|
19
|
+
/** What the plot is drawn over, in words, when it is not the whole tab ("selected on W03"). */
|
|
20
|
+
population?: string;
|
|
21
|
+
/** The population before any count ("selected on W03"), which a sweep's own hint puts its count in front of. */
|
|
22
|
+
rawPopulation?: string;
|
|
23
|
+
/** Click a level of a sweep's curve: the menu on the dies measured there. Sweeps only. */
|
|
24
|
+
onSelectLevel?: SweepPanelOptions['onSelectPoint'];
|
|
25
|
+
/** The window's heading. Default: Edit plot, or New plot for a draft. */
|
|
26
|
+
title?: string;
|
|
27
|
+
onClosed?: () => void;
|
|
28
|
+
}
|
|
29
|
+
export declare function openPlotEditor(o: PlotEditorWindowOptions): void;
|
|
30
|
+
//# sourceMappingURL=plotModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{fieldCatalogue as z,resolvePlot as I,plotTitle as $}from"../stats/plotData.js";import{plotToSweep as J}from"../stats/plotSpec.js";import{renderPlotChart as F}from"./charts/plotChart.js";import{renderSweepPanel as H}from"./charts/sweep.js";import{createPlotEditor as M}from"./plotEditor.js";import{createSweepEditor as Y}from"./sweepEditor.js";import{CLR as b,FONT as T,SPACE as i,controlStyle as B,openReparentedModal as q,wireControlHover as L}from"./toolbar.js";export function openPlotEditor(e){const{doc:n,store:c}=e;let o=e.plot,a=!e.draft;const W=z(e.items,e.ctx),h=t=>I(t,e.items,e.ctx),p=n.createElement("div");p.dataset.wmapPlotWindow="1",Object.assign(p.style,{display:"flex",flexWrap:"wrap",gap:i.xxl,alignItems:"flex-start",width:"100%",boxSizing:"border-box",padding:i.xl});const f=n.createElement("div");if(Object.assign(f.style,{flex:"3 1 420px",minWidth:"0",display:"flex",flexDirection:"column",gap:i.md}),e.population){const t=n.createElement("div");t.textContent=`Population: ${e.population}`,t.dataset.wmapPopulation="1",Object.assign(t.style,{color:b.label,fontSize:T.body}),f.appendChild(t)}const w=o.chart==="sweep";let r;if(w){const t=H({spec:J(o),dies:e.items.flatMap(l=>l.dies),testDefs:e.ctx.testDefs?[...e.ctx.testDefs]:void 0,population:e.rawPopulation??e.population,onSaveImage:e.chart.onSaveImage,onSelectPoint:e.onSelectLevel,ownerDocument:n});r={card:t.card,setPlot:l=>t.setSpec(J(l)),destroy:()=>t.destroy()}}else{const t=F({...e.chart,ownerDocument:n,waferLabel:l=>e.items[l]?.label??""});r={card:t.card,setPlot:l=>t.setPlot(h(l)),destroy:()=>t.destroy()}}r.card.style.alignSelf="stretch",f.appendChild(r.card);const m=n.createElement("div");Object.assign(m.style,{flex:"1 1 300px",minWidth:"260px",maxWidth:"420px",display:"flex",flexDirection:"column",gap:i.lg,maxHeight:"calc(min(92vh, 800px) - 110px)",overflowY:"auto",paddingRight:i.sm});const C=t=>{o=t,a&&c.upsert(t),y()},u=w?{...Y({doc:n,plot:o,testDefs:e.ctx.testDefs,onChange:C}),refreshAuto:()=>{}}:M({doc:n,plot:o,catalogue:W,followLabel:()=>e.groupLabel??(e.items.length>1?"a colour per wafer":void 0),autoText:()=>{const t=h(o);return{title:t.autoTitle,x:t.x?.label,y:t.y?.label}},onChange:C});m.appendChild(u.el);const g=n.createElement("div");Object.assign(g.style,{display:"flex",flexDirection:"column",gap:i.sm,color:b.label,fontSize:T.body,position:"sticky",bottom:"0",background:b.menuBg,borderTop:`1px solid ${b.menuBorder}`,paddingTop:i.sm,paddingBottom:i.md});const P=n.createElement("div");g.appendChild(P);const s=n.createElement("button");s.type="button",s.textContent="Add to my plots",s.dataset.wmapPlotAdd="1",Object.assign(s.style,{...B("outlined"),alignSelf:"flex-start"}),L(s),s.addEventListener("click",()=>{a=!0,c.upsert(o),N()});const E=JSON.parse(JSON.stringify(e.plot)),O=(t,l,R)=>{const d=n.createElement("button");return d.type="button",d.textContent=t,d.dataset[l]="1",Object.assign(d.style,B("outlined")),L(d),d.addEventListener("click",R),d},x=O("Reset","wmapPlotReset",()=>D()),k=O("Cancel","wmapPlotCancel",()=>{D(),v?.close()}),S=n.createElement("div");Object.assign(S.style,{display:"flex",gap:i.md,flexWrap:"wrap"}),S.append(x,k,s),g.appendChild(S),m.appendChild(g);const A=()=>JSON.stringify(o)!==JSON.stringify(E);function D(){o=JSON.parse(JSON.stringify(E)),a&&c.upsert(o),u.setPlot(o),y()}function N(){P.textContent=a?`Changes are saved to \u201C${w?o.title??"Sweep":$(h(o))}\u201D.`:"This is a draft: it is not kept until you add it to your plots.",s.style.display=a?"none":"",x.disabled=!A(),x.style.opacity=x.disabled?"0.5":"1"}function y(){r.setPlot(o),u.refreshAuto(),N()}y(),p.append(f,m);const j=c.subscribe(()=>{if(!a)return;const t=c.get().find(l=>l.id===o.id);if(!t){v?.close();return}JSON.stringify(t)!==JSON.stringify(o)&&(o=t,u.setPlot(t),y())}),v=q([p],{title:e.title??(a?"Edit plot":"New plot"),anchor:e.anchor,ownerDocument:n,boxSize:{width:"min(96vw, 1180px)",height:"min(92vh, 800px)"},onClosed:()=>{j(),r.destroy(),e.onClosed?.()}});v||(j(),r.destroy())}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { PlotSpec } from '../stats/plotSpec.js';
|
|
2
|
+
import type { SweepSpec } from '../stats/sweep.js';
|
|
3
|
+
export interface PlotStore {
|
|
4
|
+
get(): readonly PlotSpec[];
|
|
5
|
+
/** Replaces the plot with this id, or appends it. */
|
|
6
|
+
upsert(plot: PlotSpec): void;
|
|
7
|
+
/** Inserts at a position (undo of a delete puts the plot back where it was). */
|
|
8
|
+
insertAt(index: number, plot: PlotSpec): void;
|
|
9
|
+
remove(id: string): {
|
|
10
|
+
plot: PlotSpec;
|
|
11
|
+
index: number;
|
|
12
|
+
} | undefined;
|
|
13
|
+
/** A copy with a new id, placed after the original. A typed title gains "(copy)"; an untitled plot stays untitled, so its title keeps following its fields. */
|
|
14
|
+
duplicate(id: string): PlotSpec | undefined;
|
|
15
|
+
/** Replaces the whole list (an import). A sweep the host still supplies that is not in it is reported through `onRemove`, as `remove` does. */
|
|
16
|
+
replaceAll(plots: readonly PlotSpec[]): void;
|
|
17
|
+
subscribe(fn: () => void): () => void;
|
|
18
|
+
/** Sends a change still waiting for the debounce. */
|
|
19
|
+
flush(): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The title for a copy of a plot titled `title`: the original's name with a copy mark, numbered once there are several
|
|
23
|
+
* ("Vth (copy)", "Vth (copy 2)"). A copy of a copy is a copy of the original, not a copy of a copy: marks do not pile up.
|
|
24
|
+
*/
|
|
25
|
+
export declare function copyTitle(title: string, existing: ReadonlyArray<string | undefined>): string;
|
|
26
|
+
/** Sweeps a host still supplies through `insights.sweeps`: they become sweep plots in the list. */
|
|
27
|
+
export interface LegacySweeps {
|
|
28
|
+
sweeps: readonly SweepSpec[] | undefined;
|
|
29
|
+
/** Told when the reader deletes one of them, so the host stops supplying it (else the next render brings it back). */
|
|
30
|
+
onRemove?: (ids: string[]) => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The store for a host's `insights` options: its saved plots, with any `sweeps` it still supplies turned into sweep
|
|
34
|
+
* plots. A plot already saved under a sweep's id wins, so a host that keeps both does not show a sweep twice and an
|
|
35
|
+
* edit the reader made is not undone by the host's older definition.
|
|
36
|
+
*/
|
|
37
|
+
export declare function plotStoreFor(insights: {
|
|
38
|
+
plots?: PlotSpec[];
|
|
39
|
+
onPlotsChange?: (plots: PlotSpec[]) => void;
|
|
40
|
+
sweeps?: SweepSpec[];
|
|
41
|
+
onRemoveSweeps?: (ids: string[]) => void;
|
|
42
|
+
} | undefined): PlotStore;
|
|
43
|
+
export declare function createPlotStore(initial: readonly PlotSpec[] | undefined, onChange?: (plots: PlotSpec[]) => void, delayMs?: number, legacy?: LegacySweeps): PlotStore;
|
|
44
|
+
/**
|
|
45
|
+
* How a gallery hands its own store to the maps it draws on its cards. Not a public option: a card's map would
|
|
46
|
+
* otherwise build a store of its own from nothing, and the plots saved on the Plot tab would be missing from the
|
|
47
|
+
* right-click menu on every card.
|
|
48
|
+
*/
|
|
49
|
+
export interface WithPlotStore {
|
|
50
|
+
plotStore?: PlotStore;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=plotStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{sweepToPlot as O}from"../stats/plotSpec.js";import{newPlotId as g}from"../stats/plotId.js";import{noticeOnce as x}from"../renderer/deprecate.js";export function copyTitle(i,m){const l=i.replace(/(?:\s*\(copy(?: \d+)?\))+$/,"")||i,r=new Set(m);let d=`${l} (copy)`;for(let f=2;r.has(d);f++)d=`${l} (copy ${f})`;return d}export function plotStoreFor(i){return createPlotStore(i?.plots,i?.onPlotsChange,void 0,{sweeps:i?.sweeps,onRemove:i?.onRemoveSweeps})}export function createPlotStore(i,m,l=400,r){const d=new Set((i??[]).map(e=>e.id)),f=(r?.sweeps??[]).filter(e=>!d.has(e.id));(r?.sweeps?.length??0)>0&&x("insights.sweeps","`insights.sweeps` is deprecated: sweeps are plots now. Pass them in `insights.plots` as sweep plots (readPlotsFile reads a sweeps file), which the Plot tab can also save and share.");const h=new Set((r?.sweeps??[]).map(e=>e.id));let t=[...i??[],...f.map(O)].map(e=>JSON.parse(JSON.stringify(e)));const u=new Set;let p,w=!1;const S=()=>{p!==void 0&&(clearTimeout(p),p=void 0),w&&(w=!1,m?.(t.map(e=>JSON.parse(JSON.stringify(e)))))},a=()=>{for(const e of[...u])e();w=!0,p!==void 0&&clearTimeout(p),p=setTimeout(S,l)};return{get:()=>t,upsert(e){const s=t.findIndex(n=>n.id===e.id);t=s<0?[...t,e]:t.map((n,o)=>o===s?e:n),a()},insertAt(e,s){const n=Math.max(0,Math.min(e,t.length));t=[...t.slice(0,n),s,...t.slice(n)],a()},remove(e){const s=t.findIndex(o=>o.id===e);if(s<0)return;const n=t[s];return t=t.filter((o,c)=>c!==s),a(),h.has(e)&&r?.onRemove?.([e]),{plot:n,index:s}},duplicate(e){const s=t.findIndex(c=>c.id===e);if(s<0)return;const n=t[s],o=JSON.parse(JSON.stringify(n));return o.id=g(),n.title&&Object.assign(o,{title:copyTitle(n.title,t.map(c=>c.title))}),t=[...t.slice(0,s+1),o,...t.slice(s+1)],a(),o},replaceAll(e){const s=[...h].filter(n=>t.some(o=>o.id===n)&&!e.some(o=>o.id===n));t=e.map(n=>JSON.parse(JSON.stringify(n))),a(),s.length>0&&r?.onRemove?.(s)},subscribe(e){return u.add(e),()=>{u.delete(e)}},flush:S}}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Die } from '../core/dies.js';
|
|
2
|
+
import type { TestDef } from '../renderer/buildWaferMap.js';
|
|
3
|
+
import type { PlotStore } from './plotStore.js';
|
|
4
|
+
import type { InsightsItem } from './insightsTab.js';
|
|
5
|
+
import { type SaveImageHandler, type SaveTextHandler } from './toolbar.js';
|
|
6
|
+
export interface PlotSectionDeps {
|
|
7
|
+
doc: Document;
|
|
8
|
+
store: PlotStore;
|
|
9
|
+
items: InsightsItem[];
|
|
10
|
+
/** The population's reconciled test list. */
|
|
11
|
+
testDefs: TestDef[];
|
|
12
|
+
ringCount: number;
|
|
13
|
+
/** The tab's Group by key and its label, which "follow Group by" resolves to. */
|
|
14
|
+
groupBy?: string;
|
|
15
|
+
groupLabel?: string;
|
|
16
|
+
onSaveImage?: SaveImageHandler;
|
|
17
|
+
onSaveText?: SaveTextHandler;
|
|
18
|
+
/** Click a point: open its wafer, on a test's values when the plot is about one. */
|
|
19
|
+
openWafer?: (waferIndex: number, label: string, testNumber?: number) => void;
|
|
20
|
+
/** A single-wafer host's version of the same: show the test on the map that is already there. */
|
|
21
|
+
focusTest?: (testNumber: number) => void;
|
|
22
|
+
locateDie?: (die: Die, waferIndex: number | undefined) => void;
|
|
23
|
+
/** The host's own way to choose a plots file (a native dialog). Without it a file input is used. */
|
|
24
|
+
pickPlotsFile?: () => Promise<string | null>;
|
|
25
|
+
}
|
|
26
|
+
export declare function createPlotSection(d: PlotSectionDeps): {
|
|
27
|
+
card: HTMLElement;
|
|
28
|
+
destroy: () => void;
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=plotTab.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defaultPlot as Z,defaultSweep as ee,describeTitleDrift as te,examplePlots as oe,fieldCatalogue as ne,resolvePlot as se,titleDrift as le}from"../stats/plotData.js";import{addPlots as re,newPlotId as I,readPlotsFile as ae,writePlotsFile as ie}from"../stats/plotSpec.js";import{isYieldEligibleDie as ce}from"../core/dies.js";import{testValue as pe}from"../core/dieTable.js";import{sweepAppliesTo as de}from"../stats/sweep.js";import{plotToSweep as L}from"../stats/plotSpec.js";import{renderPlotChart as fe}from"./charts/plotChart.js";import{renderSweepPanel as ue}from"./charts/sweep.js";import{makeChartGridWrap as me}from"./charts/chartShell.js";import{confirmDialog as he}from"./confirmDialog.js";import{openDrilldownMenu as O}from"./drilldown.js";import{openPlotEditor as ge}from"./plotModal.js";import{sourceFromDies as ye,sourceFromPoints as we,toPlotItems as xe}from"./plotItems.js";import{CLR as h,FONT as S,RADIUS as F,SPACE as f,controlStyle as be,markNoPrint as E,saveTextFile as Pe,wireControlHover as De}from"./toolbar.js";const W=1e4;export function createPlotSection(l){const{doc:i,store:r}=l,c=xe(l.items.map(e=>({label:e.identity??e.label,dies:e.dies,waferIndex:e.waferIndex,wafer:e.wafer,passBins:e.passBins,ringCount:l.ringCount}))),C={testDefs:l.testDefs,groupBy:l.groupBy},U=e=>se(e,c,C),x=ne(c,C),u=i.createElement("div");u.dataset.wmapPlotTab="1",Object.assign(u.style,{display:"flex",flexDirection:"column",gap:f.lg,width:"100%"});const b=i.createElement("div");Object.assign(b.style,{display:"flex",flexWrap:"wrap",gap:f.md,alignItems:"center"});const d=(e,o,n)=>{const t=i.createElement("button");return t.type="button",t.textContent=e,t.dataset[o]="1",Object.assign(t.style,{...be("outlined"),fontSize:S.body}),De(t),t.addEventListener("click",n),t},P=d("Delete all plots\u2026","wmapPlotDeleteAll",()=>{V()});b.append(d("+ New plot","wmapPlotNew",()=>R()),d("+ New sweep","wmapPlotNewSweep",()=>M()),d("Add examples","wmapPlotExamples",()=>_()),d("Import plots\u2026","wmapPlotImport",()=>{Q()}),d("Export plots\u2026","wmapPlotExport",()=>X()),P),E(b),u.appendChild(b);const p=i.createElement("div");p.setAttribute("role","status"),Object.assign(p.style,{display:"none",flexWrap:"wrap",gap:f.md,alignItems:"center",padding:`${f.sm} ${f.md}`,background:h.menuBg,border:`1px solid ${h.menuBorder}`,borderRadius:F.container,color:h.text,fontSize:S.body}),E(p),u.appendChild(p);let m;function g(e,o,n=[]){m!==void 0&&clearTimeout(m),p.replaceChildren();const t=i.createElement("span");if(t.textContent=e,p.appendChild(t),o){const s=d(o.label,"wmapPlotNoticeAction",()=>{o.run(),$()});p.appendChild(s)}for(const s of n){const a=i.createElement("div");a.textContent=s,Object.assign(a.style,{flexBasis:"100%",color:h.label}),p.appendChild(a)}p.style.display="flex",m=setTimeout($,o?W:W*2)}function $(){m!==void 0&&(clearTimeout(m),m=void 0),p.style.display="none",p.replaceChildren()}const D=i.createElement("div");D.textContent="No plots yet. A plot is any measured value against another, or a distribution, that you choose: New plot starts one, and it is kept for the next lot. Add examples draws one of each chart type from this lot to start from.",Object.assign(D.style,{color:h.label,fontSize:S.body,padding:`${f.lg} 0`});const v=me(i);u.append(D,v);const y=new Map,k=()=>({onSaveImage:l.onSaveImage,onOpenWafer:l.openWafer?(e,o)=>l.openWafer(c[e].waferIndex??e,c[e].label,o):l.focusTest&&c.length===1?(e,o)=>{o!==void 0&&l.focusTest(o)}:void 0,openActionLabel:!l.openWafer&&l.focusTest&&c.length===1?"show this test on the map":void 0,onSelectPoints:(e,o,n)=>z(e,o,n)}),A=()=>({plots:r,onSaveImage:l.onSaveImage,onSaveText:l.onSaveText,onLocateDie:l.locateDie});function z(e,o,n){const t=we(e,c,l.testDefs);t&&O(o,n,t,A())}function T(e,o=!1){ge({doc:i,anchor:u,store:r,plot:e,draft:o,items:c,ctx:C,groupLabel:l.groupLabel,chart:k(),onSelectLevel:j})}function M(){const e=ee(l.testDefs,I());r.upsert(e),T(e)}function j(e,o){const n=s=>e.testNumbers.some(a=>{const w=pe(s,a);return w!==void 0&&Number.isFinite(w)}),t=ye(c,`measured at ${e.label}`,s=>ce(s)&&n(s),l.testDefs,e.testNumbers[0]);t&&O({x:o.clientX,y:o.clientY},o.target,t,A())}function R(){const e=Z(x,I());r.upsert(e),T(e)}function _(){const e=t=>JSON.stringify([t.chart,t.fields,t.aggregate??null,t.sweep??null]),o=new Set(r.get().map(e)),n=oe(x,c.length,I).filter(t=>!o.has(e(t)));if(n.length===0){g(x.some(t=>t.group==="Tests")?"The examples for this lot are already in your plots.":"There are no measured values in this lot to draw examples from.");return}for(const t of n)r.upsert(t);g(`Added ${n.length} example plot${n.length===1?"":"s"}: ${n.map(t=>t.chart).join(", ")}. Edit one to make it yours, or Delete what you do not need.`)}function N(e,o){const n=(t,s,a)=>e.appendChild(d(t,s,a));n("Edit","wmapPlotEdit",()=>{const t=r.get().find(s=>s.id===o.id);t&&T(t)}),n("Duplicate","wmapPlotDuplicate",()=>{r.duplicate(o.id)}),n("Delete","wmapPlotDelete",()=>{const t=r.remove(o.id);t&&g(`Deleted \u201C${t.plot.title??"plot"}\u201D.`,{label:"Undo",run:()=>r.insertAt(t.index,t.plot)})})}function H(e){return e.chart==="sweep"?Y(e):J(e)}function J(e){const o=fe({ownerDocument:i,waferLabel:s=>c[s]?.label??"",...k()});o.card.dataset.wmapPlotId=e.id,E(o.actions),N(o.actions,e);const n=i.createElement("div");n.dataset.wmapPlotTitleDrift="1",Object.assign(n.style,{display:"none",flexWrap:"wrap",gap:f.md,alignItems:"center",flexBasis:"100%",padding:`${f.xs} ${f.md}`,background:h.warnBg,border:`1px solid ${h.warnBorder}`,borderRadius:F.control,color:h.warnText,fontSize:S.body}),o.actions.appendChild(n);const t={card:o.card,actions:o.actions,json:"",driftNote:n,destroy:()=>o.destroy(),update:s=>{const a=U(s);o.setPlot(a),o.card.style.opacity=a.issues.length?"0.7":"1",G(n,s)}};return y.set(e.id,t),t}function Y(e){const o=L(e),n=c.flatMap(a=>a.dies),t=ue({spec:o,dies:n,testDefs:l.testDefs,onSaveImage:l.onSaveImage,ownerDocument:i,onSelectPoint:j});t.card.dataset.wmapPlotId=e.id,t.card.dataset.wmapSweepPlot="1",E(t.actions),N(t.actions,e);const s={card:t.card,actions:t.actions,json:"",destroy:()=>t.destroy(),update:a=>{const w=L(a);t.setSpec(w),t.card.style.opacity=de(w,l.testDefs)?"1":"0.7"}};return y.set(e.id,s),s}function G(e,o){const n=te(le(o,x));if(e.replaceChildren(),e.style.display=n?"flex":"none",!n)return;const t=i.createElement("span");t.textContent=n,e.append(t,d("Use the automatic title","wmapPlotAutoTitle",()=>{const s=r.get().find(w=>w.id===o.id);if(!s)return;const a={...s};delete a.title,r.upsert(a)}))}function B(){const e=r.get();for(const[o,n]of y)e.some(t=>t.id===o)||(n.destroy(),n.card.remove(),y.delete(o));e.forEach((o,n)=>{const t=y.get(o.id)??H(o),s=JSON.stringify(o);t.json!==s&&(t.json=s,t.update(o)),v.children[n]!==t.card&&v.insertBefore(t.card,v.children[n]??null)}),D.style.display=e.length===0?"":"none",P.disabled=e.length===0,P.style.opacity=e.length===0?"0.5":"1",P.style.cursor=e.length===0?"default":"pointer"}const K=r.subscribe(B);B();async function V(){const e=r.get().slice();if(e.length===0)return;const o=e.length;if(!await he({doc:i,anchor:u,title:"Delete all plots",message:o===1?"Delete your 1 saved plot? It is kept on this computer for the next lot, so it will not be there for the next file either. Export plots\u2026 first to keep a copy.":`Delete all ${o} saved plots, sweeps included? They are kept on this computer for the next lot, so they will not be there for the next file either. Export plots\u2026 first to keep a copy.`,confirmLabel:o===1?"Delete 1 plot":`Delete ${o} plots`}))return;const t=r.get().slice();t.length!==0&&(r.replaceAll([]),g(`Deleted ${t.length===1?"1 plot":`all ${t.length} plots`}.`,{label:"Undo",run:()=>r.replaceAll(t)}))}function X(){const e=r.get();if(e.length===0){g("There are no plots to export yet.");return}Pe(ie(e),"wafermap-plots.json","application/json",l.onSaveText)}async function q(){return l.pickPlotsFile?l.pickPlotsFile():new Promise(e=>{const o=i.createElement("input");o.type="file",o.accept=".json,application/json",o.addEventListener("change",()=>{const n=o.files?.[0];if(!n){e(null);return}n.text().then(e,()=>e(null))}),o.addEventListener("cancel",()=>e(null)),o.click()})}async function Q(){const e=await q();if(e===null)return;const o=ae(e);if(o.error){g(`Could not import: ${o.error}`);return}const{plots:n,copies:t}=re(r.get(),o.plots);r.replaceAll(n);const s=[...o.warnings,...t.length?[`Kept as copies, since a plot with the same id already existed: ${t.join(", ")}`]:[]];g(`Imported ${o.plots.length} plot${o.plots.length===1?"":"s"}.`,void 0,s)}return{card:u,destroy:()=>{K(),m!==void 0&&clearTimeout(m);for(const e of y.values())e.destroy();y.clear(),r.flush()}}}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type BinColors } from '../renderer/binColors.js';
|
|
2
|
-
import { type SaveImageHandler, type SaveTextHandler
|
|
2
|
+
import { type SaveImageHandler, type SaveTextHandler } from './toolbar.js';
|
|
3
|
+
import { type UserGuideExtension } from './guideWindow.js';
|
|
3
4
|
import { type Die } from '../core/dies.js';
|
|
4
5
|
import type { WaferViewOptions } from './renderWaferMap.js';
|
|
5
6
|
import type { RenderableWaferMap } from './renderWaferMap.js';
|
|
@@ -113,7 +114,7 @@ export interface GalleryOptions {
|
|
|
113
114
|
* A prefix for every file the gallery, its cards and their detached windows
|
|
114
115
|
* save — PNGs and CSVs alike. The lots, wafer or wafer count, and content
|
|
115
116
|
* follow it (`<prefix>_25-wafers_gallery-hard-bin.png`), except any the prefix
|
|
116
|
-
* already names. See docs/api.md §5.4.5.
|
|
117
|
+
* already names. See docs/api/render-map.md §5.4.5.
|
|
117
118
|
*/
|
|
118
119
|
downloadFilename?: string;
|
|
119
120
|
/**
|