@wafertools/wafermap 0.26.1 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/AGENTS.md +10 -2
  2. package/CHANGELOG.md +670 -0
  3. package/README.md +46 -3
  4. package/dist/packages/canvas-adapter/canvasTheme.d.ts +4 -0
  5. package/dist/packages/canvas-adapter/canvasTheme.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +16 -0
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/binCluster.d.ts +0 -1
  9. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +21 -2
  11. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/capability.d.ts +10 -11
  13. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +337 -3
  15. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  16. package/dist/packages/canvas-adapter/charts/correlation.d.ts +8 -10
  17. package/dist/packages/canvas-adapter/charts/correlation.js +2 -1
  18. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +50 -0
  19. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -0
  20. package/dist/packages/canvas-adapter/charts/histogram.d.ts +12 -2
  21. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  22. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +0 -1
  23. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  24. package/dist/packages/canvas-adapter/charts/scatter.d.ts +6 -1
  25. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  26. package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +20 -0
  27. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -0
  28. package/dist/packages/canvas-adapter/charts/trend.d.ts +37 -0
  29. package/dist/packages/canvas-adapter/charts/trend.js +1 -0
  30. package/dist/packages/canvas-adapter/dieList.js +7 -7
  31. package/dist/packages/canvas-adapter/hitGrid.d.ts +22 -0
  32. package/dist/packages/canvas-adapter/hitGrid.js +1 -0
  33. package/dist/packages/canvas-adapter/identityHeader.d.ts +46 -0
  34. package/dist/packages/canvas-adapter/identityHeader.js +1 -0
  35. package/dist/packages/canvas-adapter/index.d.ts +2 -1
  36. package/dist/packages/canvas-adapter/index.js +1 -1
  37. package/dist/packages/canvas-adapter/insightsTab.d.ts +51 -5
  38. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  39. package/dist/packages/canvas-adapter/maplessSummary.d.ts +5 -1
  40. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  41. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +15 -2
  42. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -20
  43. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +110 -18
  44. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  45. package/dist/packages/canvas-adapter/summaryPanel.d.ts +233 -37
  46. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  47. package/dist/packages/canvas-adapter/toCanvas.d.ts +6 -0
  48. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  49. package/dist/packages/canvas-adapter/toolbar.d.ts +352 -15
  50. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  51. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  52. package/dist/packages/canvas-adapter/userGuideHtml.js +115 -41
  53. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  54. package/dist/packages/canvas-adapter/version.js +1 -1
  55. package/dist/packages/canvas-adapter/warnings.d.ts +21 -0
  56. package/dist/packages/canvas-adapter/warnings.js +1 -1
  57. package/dist/packages/core/dies.d.ts +11 -0
  58. package/dist/packages/core/dies.js +1 -1
  59. package/dist/packages/core/utils.d.ts +11 -0
  60. package/dist/packages/core/utils.js +1 -1
  61. package/dist/packages/renderer/binColors.d.ts +80 -0
  62. package/dist/packages/renderer/binColors.js +1 -0
  63. package/dist/packages/renderer/buildView.d.ts +100 -6
  64. package/dist/packages/renderer/buildView.js +1 -1
  65. package/dist/packages/renderer/buildWaferMap.d.ts +112 -7
  66. package/dist/packages/renderer/buildWaferMap.js +1 -1
  67. package/dist/packages/renderer/colorMap.d.ts +16 -15
  68. package/dist/packages/renderer/colorMap.js +1 -1
  69. package/dist/packages/renderer/colorSchemes.d.ts +45 -26
  70. package/dist/packages/renderer/colorSchemes.js +1 -1
  71. package/dist/packages/renderer/index.d.ts +3 -1
  72. package/dist/packages/renderer/index.js +1 -1
  73. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  74. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  75. package/dist/packages/stats/binPareto.d.ts +15 -0
  76. package/dist/packages/stats/binPareto.js +1 -1
  77. package/dist/packages/stats/clusterDetection.js +1 -1
  78. package/dist/packages/stats/connectedComponents.d.ts +14 -0
  79. package/dist/packages/stats/connectedComponents.js +1 -0
  80. package/dist/packages/stats/correlation.d.ts +26 -0
  81. package/dist/packages/stats/correlation.js +1 -1
  82. package/dist/packages/stats/facets.js +1 -1
  83. package/dist/packages/stats/filterFindings.d.ts +18 -0
  84. package/dist/packages/stats/filterFindings.js +1 -1
  85. package/dist/packages/stats/findingsNarrative.js +1 -1
  86. package/dist/packages/stats/histogram.d.ts +47 -2
  87. package/dist/packages/stats/histogram.js +1 -1
  88. package/dist/packages/stats/index.d.ts +6 -0
  89. package/dist/packages/stats/index.js +1 -1
  90. package/dist/packages/stats/mergeTestDefs.d.ts +44 -0
  91. package/dist/packages/stats/mergeTestDefs.js +1 -0
  92. package/dist/packages/stats/metadataColumns.d.ts +20 -4
  93. package/dist/packages/stats/metadataColumns.js +1 -1
  94. package/dist/packages/stats/patternClassification.js +1 -1
  95. package/dist/packages/stats/population.d.ts +23 -0
  96. package/dist/packages/stats/population.js +1 -0
  97. package/dist/packages/stats/renderFindingsReport.js +15 -15
  98. package/dist/packages/stats/renderSummaryReport.js +35 -32
  99. package/dist/packages/stats/reportHtml.js +38 -13
  100. package/dist/packages/stats/testPassRate.d.ts +85 -0
  101. package/dist/packages/stats/testPassRate.js +1 -0
  102. package/dist/packages/stats/trend.d.ts +40 -0
  103. package/dist/packages/stats/trend.js +1 -0
  104. package/dist/packages/stats/types.d.ts +117 -6
  105. package/dist/packages/stats/yield.js +1 -1
  106. package/llms.txt +6 -0
  107. package/package.json +9 -5
  108. package/dist/packages/canvas-adapter/metadataBadge.d.ts +0 -30
  109. package/dist/packages/canvas-adapter/metadataBadge.js +0 -1
@@ -3,11 +3,29 @@ import type { Die, PositionedDie } from '../core/dies.js';
3
3
  import type { Reticle } from '../core/reticle.js';
4
4
  import type { DieMetadata, WaferMetadata } from '../core/metadata.js';
5
5
  import { type Affine } from '../core/transforms.js';
6
+ import { type BinColors } from './binColors.js';
6
7
  import type { TestDef, BinDef, MetadataFieldDef, ReticleConfig } from './buildWaferMap.js';
7
8
  /** Resolve a toolbar activeTest cursor to the canonical test number for getDieTestValue. */
8
9
  export declare function resolveTestNumber(activeTest: number, testDefs?: TestDef[]): {
9
10
  testNumber: number;
10
11
  };
12
+ /**
13
+ * The distinct metadata values a `metadata`-mode view will colour, in the exact
14
+ * order colour indices are assigned to them.
15
+ *
16
+ * The one implementation of that ordering, because two existed and they only
17
+ * agreed by luck: `buildView` derived it per view while `renderWaferGallery`'s
18
+ * lot-wide legend strip derived its own from `String(raw)` over every card —
19
+ * a different value derivation (`metadataCategoricalValue` formats numbers and
20
+ * rejects objects) as well as a different population. A comment asked the two to
21
+ * stay in step; nothing made them.
22
+ *
23
+ * Natural order, so D0, D1, D2, D10 reads correctly instead of the
24
+ * lexicographic D0, D1, D10, D2. Partial and edge-excluded dies are skipped:
25
+ * they never reach the metadata fill branch, so letting them rank values here
26
+ * would shift colours for values no visible die shows.
27
+ */
28
+ export declare function collectMetadataValues(dies: readonly Die[], key: string): string[];
11
29
  /** Find a TestDef by its testNumber. Returns undefined when testDefs is absent or no match. */
12
30
  export declare function findTestDef(testDefs: TestDef[] | undefined, testNumber: number): TestDef | undefined;
13
31
  /** Return sorted unique test numbers present across a set of dies (from testValues keys). */
@@ -75,14 +93,54 @@ export interface ViewOverlay {
75
93
  lineWidth: number;
76
94
  fill?: string;
77
95
  }
96
+ /**
97
+ * A renderer-agnostic description of one drawn wafer map: everything `toCanvas`
98
+ * needs, in physical (mm) coordinates, with no canvas or DOM dependency.
99
+ *
100
+ * This is the contract between `buildView` and a renderer — see the `pipeline`
101
+ * example for the low-level path. Hosts using `buildWaferMap` + `renderWaferMap`
102
+ * never need it: that pipeline builds and re-builds the view internally, and the
103
+ * copy hung on `WaferMapResult.view` is marked `@internal` precisely because
104
+ * reading it there means reaching past the API that owns it.
105
+ *
106
+ * Treat the geometry arrays as read-only. `dies` and `hoverPoints` are
107
+ * **index-parallel** — `hoverPoints[i]` is the transformed centre of `dies[i]` —
108
+ * and code that resolves a die from a screen position depends on that staying
109
+ * true.
110
+ */
78
111
  export interface View {
112
+ /** Die rectangles to fill, in draw order, already carrying their resolved colour. */
79
113
  rectangles: ViewRect[];
114
+ /**
115
+ * Transformed centre of each die, index-parallel with {@link dies}. Separate
116
+ * from `rectangles` because a die always has a centre for hit-testing and
117
+ * viewport fitting even when it is not drawn (a partial die with
118
+ * `showPartialDies: false`).
119
+ */
80
120
  hoverPoints: ViewHoverPoint[];
121
+ /** Text to draw — die labels and any axis/indicator strings the view generated. */
81
122
  texts: ViewText[];
123
+ /** Non-die geometry: wafer outline, notch, ring/quadrant boundaries, reticle grid, XY indicator. */
82
124
  overlays: ViewOverlay[];
125
+ /** The plot mode this view was built for. Colours in `rectangles` follow it. */
83
126
  plotMode: PlotMode;
84
- colorScheme: string;
127
+ /** Bin palette used for hard/soft-bin modes, as registered with `registerBinColorScheme`. */
128
+ binColorScheme: string;
129
+ /** Value gradient used for value and stacked modes, as registered with `registerValueColorScheme`. */
130
+ valueColorScheme: string;
131
+ /**
132
+ * Every bin's resolved colour, both bin types — the single source for bin
133
+ * colour. Die fills are built from it, and legends, panels and charts must
134
+ * read it rather than re-deriving a colour, or they can disagree with the map.
135
+ */
136
+ binColors: BinColors;
137
+ /** Wafer-level metadata carried through from the wafer, or `null`. */
85
138
  metadata: WaferMetadata | null;
139
+ /**
140
+ * The positioned dies this view was built from, index-parallel with
141
+ * {@link hoverPoints}. Original grid coordinates are preserved on each die —
142
+ * never derive display coordinates from these.
143
+ */
86
144
  dies: Die[];
87
145
  /** Actual [min, max] of the value data used for color normalization. */
88
146
  valueRange: [number, number];
@@ -220,8 +278,22 @@ export interface ViewOptions {
220
278
  showQuadrantBoundaries?: boolean;
221
279
  showXYIndicator?: boolean;
222
280
  dieGap?: number;
223
- /** Named colour scheme — any scheme registered via registerColorScheme(). Default: 'default'. */
224
- colorScheme?: string;
281
+ /** Bin palette for `hardBin`/`softBin` — any name registered via `registerBinColorScheme()`. Default `'default'`. */
282
+ binColorScheme?: string;
283
+ /** Value gradient for `value` and the stacked modes — any name registered via `registerValueColorScheme()`. Default `'default'`. */
284
+ valueColorScheme?: string;
285
+ /** Honour `BinDef.color` where a bin definition supplies one. Default true. See `resolveBinColors`. */
286
+ useDefinedBinColors?: boolean;
287
+ /**
288
+ * Bin colours resolved over a WIDER population than these dies — a host
289
+ * showing several wafers together passes one `resolveBinColors` result to
290
+ * every map so a bin is the same colour on each (`renderWaferGallery` does
291
+ * this for you). The bin counterpart of `metadataValueOrder`.
292
+ *
293
+ * Ignored, and colours resolved from these dies instead, when it lacks any
294
+ * bin present here: a stale assignment must never leave a real bin uncoloured.
295
+ */
296
+ binColors?: BinColors;
225
297
  highlightBin?: number;
226
298
  /** Dim every die except this metadata value, `'metadata'` mode's analogue of `highlightBin`. */
227
299
  highlightMetadataValue?: string;
@@ -246,6 +318,27 @@ export interface ViewOptions {
246
318
  test: number;
247
319
  range: [number, number];
248
320
  };
321
+ /**
322
+ * The full, ordered list of metadata values to assign colours from, for
323
+ * `metadata` plot mode — the exact counterpart of `valueRange`'s `{ test,
324
+ * range }` form, and applied only when `key` matches `activeMetadataKey`.
325
+ *
326
+ * Without it, each view assigns colours by index into the values present on
327
+ * ITS OWN dies. That is right for a standalone map and wrong for a gallery:
328
+ * a wafer that never exhibited one defect category has a shorter list, so
329
+ * every later value shifts up a colour and the card disagrees with the
330
+ * lot-wide legend beside it about what that colour means. A host showing
331
+ * several wafers together passes the lot-wide order here so every card
332
+ * indexes into the same list.
333
+ *
334
+ * Values present on these dies but absent from the list are appended (in
335
+ * natural order) rather than dropped, so a short or stale list can only cost
336
+ * the shared ordering, never leave real dies uncoloured.
337
+ */
338
+ metadataValueOrder?: {
339
+ key: string;
340
+ values: string[];
341
+ };
249
342
  /**
250
343
  * Controls the default colorbar range when the active testDef has spec limits.
251
344
  * `'spec'` (default when limits present): colorbar spans [limitLow, limitHigh].
@@ -255,8 +348,8 @@ export interface ViewOptions {
255
348
  /**
256
349
  * Which bins count as passing. Default `[1]`.
257
350
  *
258
- * Used only to set `ViewRect.binFail`, never to colour anything — bin colour
259
- * comes from the colour scheme regardless of pass/fail. Must match the
351
+ * Sets `ViewRect.binFail` and decides which bins take the palette's pass
352
+ * colours (see `resolveBinColors`) — never the bin number. Must match the
260
353
  * `passBins` given to `buildWaferMap` / `analyzeWaferMap`, or the map will
261
354
  * disagree with the yield figure beside it.
262
355
  */
@@ -370,7 +463,6 @@ export interface HoverTextOptions {
370
463
  aggrMethod?: string;
371
464
  /** Number of wafers in the stack, used to annotate a count with its share of the lot. */
372
465
  lotSize?: number;
373
- waferMeta?: WaferMetadata | null;
374
466
  /** Active test number (value mode only) — leads the tooltip and gets an out-of-spec note. */
375
467
  activeTest?: number;
376
468
  /** Reticle geometry, when configured — appended as a "Reticle (col, row)" line below Die (x, y). */
@@ -425,6 +517,8 @@ export declare function generateTextOverlay(dies: PositionedDie[], txCoords: Flo
425
517
  /** Active `die.metadata` key + its resolved value→color map, `'metadata'` mode only. */
426
518
  activeMetadataKey?: string;
427
519
  metadataColorMap?: Map<string, string> | null;
520
+ /** Mirrors `View.allIntegerValues` — see the colorbar's own `tickFmt` override in toCanvas.ts. */
521
+ allIntegerValues?: boolean;
428
522
  }): ViewText[];
429
523
  /**
430
524
  * Build a renderer-agnostic scene from a wafer, dies, and display options.
@@ -1 +1 @@
1
- import{hasPosition as Zt}from"../core/dies.js";import{getReticleCell as te}from"../core/reticle.js";import{metadataCategoricalValue as ee,metadataDisplayValue as ne}from"../core/metadata.js";import{affineRotation as dt,affineMirror as Rt,affineCompose as it,affinePoint as Y,affineVector as ht,affineSwapsAxes as ae}from"../core/transforms.js";import{contrastTextColor as G,SPEC_PASS_FILL as pt,SPEC_FAIL_LOW as ie,SPEC_FAIL_HIGH as mt}from"./colorMap.js";import{getColorScheme as se}from"./colorSchemes.js";import{getDieTestValue as R,getTestPassStatus as Q,isParametricTest as yt}from"./buildWaferMap.js";import{fmt as gt,fmtColorbarAxis as bt,fmtAggregationMethod as Ct}from"./fmt.js";import{metadataValueColor as oe}from"./colorMap.js";import{compareNatural as re,clamp01 as Nt}from"../core/utils.js";import{prettyKey as Ht}from"../core/utils.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function findTestDef(t,e){return t?.find(a=>a.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,a)=>e-a)}const le="#d3d6db",Ot="#e8e9ea",ce="#aab0ba",j="#d6d9dd";function st(t,e){return e?ee(t.metadata?.[e]):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function ue(t,e,a){const{x:i,y:s}=t.center,n=dt(t.orientation,i,s),o=it(Rt(e?.x??!1,e?.y??!1,i,s),dt(t.orientation,i,s)),r=it(Rt(a?.flipX??!1,a?.flipY??!1,i,s),dt(a?.rotation??0,i,s));return{bakedToScreen:r,physicalToScreen:it(r,n),gridToScreen:it(r,o)}}function W(t,e=!1){return{points:[t],closed:e}}function fe(t,e){const{center:a,radius:i,notch:s}=t;let n=a.x+i*Math.cos(e),o=a.y+i*Math.sin(e);if(!s)return{x:n,y:o};const r=Math.sqrt(i**2-(s.length/2)**2),u=s.length/2,b=n-a.x,y=o-a.y;return s.type==="bottom"&&y<-r?(o=a.y-r,n=a.x+Math.max(-u,Math.min(u,b))):s.type==="top"&&y>r?(o=a.y+r,n=a.x+Math.max(-u,Math.min(u,b))):s.type==="left"&&b<-r?(n=a.x-r,o=a.y+Math.max(-u,Math.min(u,y))):s.type==="right"&&b>r&&(n=a.x+r,o=a.y+Math.max(-u,Math.min(u,y))),{x:n,y:o}}function At(t,e){return t.map(e).join(" / ")}function de(t,e=256){const a=Array.from({length:e},(i,s)=>t(s/(e-1)));return i=>a[Math.min(e-1,Math.max(0,Math.round(i*(e-1))))]}function he(t,e){const a=Math.max(1,Math.min(t.width,t.height)),i=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(a*.55,i*1.8))))}function pe(t,e,a){if(e?.length){const i=e.flatMap(s=>{const n=s.testNumber;if(!yt(s)){const u=Q(t,n,s);return u===void 0?[]:[{key:n,label:s.name,value:u?"Pass":"Fail"}]}const o=R(t,n);if(o===void 0)return[];const r=t.testPass?.[n]===!1?" <i>(recorded fail)</i>":"";return[{key:n,label:s.name,value:`${gt(o,s.unit,a)}${r}`}]});if(i.length)return i}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([i,s])=>({key:Number(i),label:`Test ${i}`,value:gt(s,void 0,a)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([i,s])=>({key:Number(i),label:`Test ${i}`,value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,a={}){const{testDefs:i,hbinDefs:s,sbinDefs:n,fallbackFormat:o,aggrMethod:r,lotSize:u,waferMeta:b,activeTest:y,reticleConfig:B,metadataFields:V}=a,v=s?new Map(s.map(c=>[c.bin,c])):null,T=n?new Map(n.map(c=>[c.bin,c])):null,m=[`Die (${t.x}, ${t.y})`];if(B&&Zt(t)){const c=te(t,B);m.push(`Reticle (${c.column}, ${c.row})`)}if(e==="stackedValues"){const c=R(t,0);if(c!==void 0){const f=i?.[0],h=f?.testNumber,g=f?.name??(h!=null?`Test ${h}`:"Value"),M=r?` (${r})`:"";m.push(`${g}${M}: ${gt(c,f?.unit,o)}`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const c=R(t,0),f=e==="stackedSoftBins"?t.sbin:t.hbin;if(c!==void 0){const h=e==="stackedSoftBins"?T:v,g=f!==void 0?h?.get(f)?.name?`${f} \xB7 ${h.get(f).name}`:`Bin ${f}`:"Bin";let M;if(r==="percent")M=`${c.toFixed(0)}%`;else{const H=u?` (${(c/u*100).toFixed(0)}% of lot)`:"";M=`${c}${H}`}const p=r?` [${Ct(r)}]`:"";m.push(`${g}: ${M}${p}`)}}else{const c=pe(t,i,o);if(e==="value"&&c.length){const{testNumber:f}=resolveTestNumber(y??0,i),h=findTestDef(i,f),g=R(t,f);let M=c.findIndex(E=>E.key===f);M<0&&(M=0);const p=c[M];let H=`<b>${p.label}: ${p.value}</b>`;if(p.key===f&&h){const E=classifySpec(g,h);(E==="failLow"||E==="failHigh")&&(H+=" <i>(out of spec)</i>")}m.push(H);const A=c.length-1;A>0&&m.push(`<i>+${A} more test${A===1?"":"s"}</i>`)}else if(c.length){const f=c.length;m.push(`<i>${f} test result${f===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const f=[];if(t.hbin!==void 0){const h=v?.get(t.hbin)?.name,g=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);f.push(`HBin: ${g}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,g=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);f.push(`SBin: ${g}`)}m.push(f.join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&m.push(`Retests: ${t.retestCount}`),t.siteNum!==void 0&&m.push(`Site: ${t.siteNum}`),t.partId!==void 0&&m.push(`Part ID: ${t.partId}`),t.partial&&m.push("<i>partial die</i>"),t.probeIndex!==void 0&&m.push(`Probe: #${t.probeIndex}`);const S={...b??{},...t.metadata??{}};for(const[c,f]of Object.entries(S)){const h=ne(f);if(h===void 0)continue;const g=V?.find(M=>M.key===c)?.label??Ht(c);m.push(`${g}: ${h}`)}return m.join("<br>")}export function buildMapTitle(t,e="engineering",a){const i=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=n=>{const o=a?.length===1?a[0]:void 0;return o===void 0?`${n} Bin`:o.name?`${n} Bin ${o.bin} \xB7 ${o.name}`:`${n} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:i};case"stackedSoftBins":return{primary:s("Soft"),secondary:i};case"metadata":{const n=t.metadataFields?.find(r=>r.key===t.activeMetadataKey),o=t.activeMetadataKey?Ht(t.activeMetadataKey):"Metadata";return{primary:n?.label??o,secondary:""}}case"stackedValues":{const n=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:r}=bt(o,n?.name,n?.unit,e),u=r||"Value",b=t.aggrMethod?` \xB7 ${Ct(t.aggrMethod)}`:"";return{primary:`${u}${b}`,secondary:i}}default:{const n=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:r}=bt(o,n?.name,n?.unit,e),u=r||n?.name;if(t.passFailDisplay!=="off"){const b=n?.testNumber??t.activeTest,y=u?`${u} \xB7 #${b}`:`Test ${b}`,B=t.passFailDisplay==="spec"?"Spec pass/fail":n!==void 0&&!yt(n)?"Functional pass/fail":"Tester pass/fail";return{primary:y,secondary:B}}return{primary:u??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,a){const{plotMode:i,colorFns:s,normalize:n,activeTest:o,valueRange:r,testDefs:u,fallbackFormat:b,passFailDisplay:y="off",activeMetadataKey:B,metadataColorMap:V}=a,v=findTestDef(u,o),{tickFmt:T}=bt(r[1],v?.name,v?.unit,b),{testNumber:m}=resolveTestNumber(o,u);return t.flatMap((S,c)=>{let f="",h="#111111";if(i==="value"&&y==="test"){const p=Q(S,m,v);if(p===void 0)return[];f=p?"P":"F",h=G(p?pt:mt)}else if(i==="value"){const p=R(S,m);if(p===void 0)return[];f=At([p],T),h=G(s.forValue(n(p)))}else if(i==="hardBin"||i==="softBin"){const p=i==="softBin"?S.sbin:S.hbin;if(p===void 0)return[];f=String(p),h=G(s.forBin(p))}else if(i==="metadata"){const p=st(S,B);if(p===void 0)return[];f=p,h=G(V?.get(f)??j)}else{const p=R(S,0);if(p===void 0)return[];f=At([p],T),h=G(s.forValue(n(p)))}const g=e?e[c*2]:S.physX,M=e?e[c*2+1]:S.physY;return[{x:g,y:M,text:f,fontSize:he(S,f),color:h,align:"center"}]})}function Et(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function me(t,e,a=720){const{center:i,radius:s,notch:n}=t,o=m=>Y(e,m.x,m.y);if(!(n!==void 0&&t.diameter>150)){const m=[];for(let S=0;S<=a;S++){const c=2*Math.PI*S/a;m.push(o(fe(t,c)))}return[{kind:"wafer-boundary",...W(m,!0),lineColor:"#888888",lineWidth:1}]}const u=1.25,b=n.length,y=Et(n.type),B=Math.atan2(b,s),V=s-u,v=[];let T=!1;for(let m=0;m<=a;m++){const S=2*Math.PI*m/a;let c=S-y;for(;c>Math.PI;)c-=2*Math.PI;for(;c<-Math.PI;)c+=2*Math.PI;if(Math.abs(c)<=B){if(!T){T=!0;const f={x:i.x+s*Math.cos(y-B),y:i.y+s*Math.sin(y-B)},h={x:i.x+V*Math.cos(y),y:i.y+V*Math.sin(y)},g={x:i.x+s*Math.cos(y+B),y:i.y+s*Math.sin(y+B)};v.push(o(f),o(h),o(g))}continue}v.push(o({x:i.x+s*Math.cos(S),y:i.y+s*Math.sin(S)}))}return[{kind:"wafer-boundary",...W(v,!0),lineColor:"#888888",lineWidth:1}]}function ye(t,e,a,i=360){const s=[],n=Math.max(1,Math.floor(a));for(let o=1;o<n;o++){const r=t.radius*o/n,u=[];for(let b=0;b<=i;b++){const y=2*Math.PI*b/i,B={x:t.center.x+r*Math.cos(y),y:t.center.y+r*Math.sin(y)};u.push(Y(e,B.x,B.y))}s.push({kind:"ring-boundary",...W(u,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function ge(t,e,a,i){const{x:s,y:n}=t.center,o=t.radius,r=(S,c)=>Y(e,S,c),u=a-s,b=Math.sqrt(Math.max(0,o*o-u*u)),y=r(a,n-b),B=r(a,n+b),V=i-n,v=Math.sqrt(Math.max(0,o*o-V*V)),T=r(s-v,i),m=r(s+v,i);return[{kind:"quadrant-boundary",...W([y,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...W([T,m]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function be(t,e){return t.map(a=>{const i=a.width/2,s=a.height/2;return{kind:"reticle",points:[[[a.x-i,a.y-s],[a.x+i,a.y-s],[a.x+i,a.y+s],[a.x-i,a.y+s]].map(([o,r])=>Y(e,o,r))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function xe(t){const e=t.filter(a=>a.probeIndex!==void 0).sort((a,i)=>(a.probeIndex??0)-(i.probeIndex??0));return e.length?[{kind:"probe-path",...W(e.map(a=>({x:a.physX,y:a.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function Me(t,e,a,i,s,n,o,r,u,b,y,B,V,v,T="off",m,S,c){const f=e.width-o,h=e.height-o,g=n?h:f,M=n?f:h,p=$=>s==="softBin"?$.sbin:$.hbin,H=$=>st($,m);if(e.partial){t.push({x:a,y:i,width:g,height:M,fill:le,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:a,y:i,width:g,height:M,fill:ce,type:"stacked",metadata:e.metadata});return}if(u!==void 0&&(s==="hardBin"||s==="softBin")&&p(e)!==u){t.push({x:a,y:i,width:g,height:M,fill:Ot,type:"hardBin",metadata:e.metadata});return}if(c!==void 0&&s==="metadata"&&H(e)!==c){t.push({x:a,y:i,width:g,height:M,fill:Ot,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(T==="test"){const J=Q(e,y,v),ot=J===void 0?j:J?pt:mt;t.push({x:a,y:i,width:g,height:M,fill:ot,type:"value",metadata:e.metadata});return}const $=R(e,y),D=classifySpec($,v);let _,U;$===void 0?_=j:T==="spec"?_=D==="failLow"?ie:D==="failHigh"?mt:pt:(_=r.forValue(b($)),(D==="failLow"||D==="failHigh")&&(U=D)),t.push({x:a,y:i,width:g,height:M,fill:_,type:"value",specMark:U,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const $=p(e),D=$!=null?r.forBin($):j,_=$!=null&&!V.has($)?!0:void 0;t.push({x:a,y:i,width:g,height:M,fill:D,type:s,binFail:_,metadata:e.metadata});return}if(s==="metadata"){const $=H(e),D=$!==void 0?S?.get($)??j:j;t.push({x:a,y:i,width:g,height:M,fill:D,type:"metadata",metadata:e.metadata});return}const A=R(e,0),E=A!==void 0?r.forValue(b(A)):j;t.push({x:a,y:i,width:g,height:M,fill:E,type:"value",metadata:e.metadata})}function Se(t,e,a){const i=t.radius*.15,s=ht(e,i,0),n=ht(e,0,i),o=s.x+n.x>=0?-1:1,r=s.y+n.y>=0?-1:1,u={x:t.center.x+o*t.radius*.9,y:t.center.y+r*t.radius*.9},b={x:u.x+s.x,y:u.y+s.y},y={x:u.x+n.x,y:u.y+n.y};return a.push({x:b.x+s.x*.35,y:b.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:y.x+n.x*.35,y:y.y+n.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...W([u,b]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...W([u,y]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,a={},i){const s=a.reticles??[],{plotMode:n="value",showDieLabels:o=!1,showReticle:r=!1,showProbePath:u=!1,ringCount:b=4,showRingBoundaries:y=!1,showQuadrantBoundaries:B=!1,showXYIndicator:V=!1,dieGap:v=1,colorScheme:T="default",passBins:m=[1],highlightBin:S,highlightMetadataValue:c,interactiveTransform:f,valueRange:h,testDefs:g,activeTest:M=0,activeMetadataKey:p,fallbackFormat:H="engineering",aggregationMethod:A,lotSize:E,dataAxisFlip:$,isLotStack:D=!1,logScale:_,colorbarRangeMode:U="spec",passFailDisplay:J,showPartialDies:ot=!0}=a,xt=J??"off",Mt=i?.hbinDefs,St=i?.sbinDefs,$t=i?.metadataFields,_t=$t?.find(l=>l.key===p),wt={x:($?.x??!1)!==(f?.flipX??!1),y:($?.y??!1)!==(f?.flipY??!1)},Wt=Mt?new Map(Mt.map(l=>[l.bin,l])):null,jt=St?new Map(St.map(l=>[l.bin,l])):null,rt=n==="softBin"?jt:Wt,lt=se(T),zt=new Set(m),kt={forValue:de(lt.forValue),forBin:T==="custom"&&rt?l=>rt.get(l)?.color??lt.forBin(l):lt.forBin};let{testNumber:O}=resolveTestNumber(M,g);if(!g?.length&&n==="value"&&!e.some(d=>d.testValues&&O in d.testValues)){const d=e.reduce((x,F)=>{if(!F.testValues)return x;const k=Object.keys(F.testValues).map(Number),q=k.length?Math.min(...k):void 0;return q!==void 0&&(x===void 0||q<x)?q:x},void 0);d!==void 0&&(O=d)}let Z;if(Array.isArray(h))Z=h;else if(h){const{testNumber:l}=resolveTestNumber(h.test,g);Z=l===O?h.range:void 0}const I=findTestDef(g,O),Bt=n==="value"&&I!==void 0&&!yt(I),Kt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let C="off";n==="value"&&(Bt?C="test":xt==="spec"&&Kt?C="spec":xt==="test"&&e.some(l=>Q(l,O,I)!==void 0)&&(C="test"));const vt=C==="spec"?"spec":U;let P,N,Tt=!1;if(Z)[P,N]=Z;else{const l=n==="stackedValues"||n==="stackedBins"||n==="stackedSoftBins",d=!l&&n==="value"&&vt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let x=1/0,F=-1/0,k=!0;for(const q of e){const X=l?R(q,0):R(q,O);X!==void 0&&(X<x&&(x=X),X>F&&(F=X),Number.isInteger(X)||(k=!1))}isFinite(x)||(k=!1),Tt=k,d?(P=I.limitLow!==void 0?I.limitLow:isFinite(x)?x:0,N=I.limitHigh!==void 0?I.limitHigh:isFinite(F)?F:1):(P=isFinite(x)?x:0,N=isFinite(F)?F:1)}P===N&&(N=P+1);const It=Bt?!1:_??I?.logScale??!1,Ft=It&&P>0&&N>0,qt=Ft;let tt;if(Ft){const l=Math.log10(P),d=Math.log10(N)-l;tt=x=>x<=0?0:Nt((Math.log10(x)-l)/d)}else tt=l=>Nt((l-P)/(N-P));const L=ue(t,$,f);let Lt=null;if(t.notch){const l=Et(t.notch.type),d=ht(L.physicalToScreen,Math.cos(l)*t.radius,Math.sin(l)*t.radius),x=Math.hypot(d.x,d.y);x>0&&(Lt={x:d.x/x,y:d.y/x})}let z=1/0;for(let l=0;l<e.length;l++){const d=e[l];d.width<z&&(z=d.width),d.height<z&&(z=d.height)}const Xt=z!==1/0?Math.min(v,z*.12):v,Vt=[],ct=[],Yt=L.bakedToScreen.a===1&&L.bakedToScreen.b===0&&L.bakedToScreen.c===0&&L.bakedToScreen.d===1&&L.bakedToScreen.e===0&&L.bakedToScreen.f===0;let w=null;if(!Yt){w=new Float64Array(e.length*2);for(let l=0;l<e.length;l++){const d=Y(L.bakedToScreen,e[l].physX,e[l].physY);w[l*2]=d.x,w[l*2+1]=d.y}}const Gt=ae(L.gridToScreen),Qt=n==="hardBin"||n==="softBin"||n==="stackedBins"||n==="stackedSoftBins",Ut=n==="softBin"||n==="stackedSoftBins";let et=Qt?new Map:void 0;const ut=n==="value"&&C==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,ft=n==="value"&&C==="test"?{pass:0,fail:0}:void 0,Dt=n==="metadata";let nt=Dt?new Map:void 0,at=null;if(Dt&&p){const l=new Set;for(const x of e){if(x.partial||x.edgeExcluded)continue;const F=st(x,p);F!==void 0&&l.add(F)}const d=[...l].sort(re);at=new Map(d.map((x,F)=>[x,_t?.values?.find(k=>k.value===x)?.color??oe(F)]))}for(let l=0;l<e.length;l++){const d=e[l],x=w?w[l*2]:d.physX,F=w?w[l*2+1]:d.physY;if(ct.push({x,y:F}),et&&!d.partial&&!d.edgeExcluded){const k=Ut?d.sbin:d.hbin;k!=null&&et.set(k,(et.get(k)??0)+1)}if(ut&&!d.partial&&!d.edgeExcluded){const k=classifySpec(R(d,O),I);k&&ut[k]++}if(ft&&!d.partial&&!d.edgeExcluded){const k=Q(d,O,I);k!==void 0&&ft[k?"pass":"fail"]++}if(nt&&!d.partial&&!d.edgeExcluded){const k=st(d,p);k!==void 0&&nt.set(k,(nt.get(k)??0)+1)}d.partial&&!ot||Me(Vt,d,x,F,n,Gt,Xt,kt,S,tt,O,rt,zt,I,C,p,at,c)}const Pt=o?generateTextOverlay(e,w,{plotMode:n,colorFns:kt,normalize:tt,activeTest:M,valueRange:[P,N],testDefs:g,fallbackFormat:H,passFailDisplay:C,activeMetadataKey:p,metadataColorMap:at}):[],K=me(t,L.physicalToScreen);y&&K.push(...ye(t,L.physicalToScreen,b)),B&&K.push(...ge(t,L.physicalToScreen,t.center.x,t.center.y)),r&&K.push(...be(s,L.gridToScreen)),u&&K.push(...xe(e)),V&&K.push(...Se(t,L.gridToScreen,Pt));const Jt=ct.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Vt,hoverPoints:ct,texts:Pt,overlays:K,plotMode:n,colorScheme:T,metadata:t.metadata??null,dies:e,valueRange:[P,N],allIntegerValues:Tt,testDefs:g,activeTest:M,logScale:qt,logScaleRequested:It,aggrMethod:A,lotSize:E,axisFlip:wt,dataAxisFlip:{x:$?.x??!1,y:$?.y??!1},gridToScreen:L.gridToScreen,rotation:((t.orientation+(f?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:D,passFailDisplay:C,colorBySpec:C==="spec",colorbarRangeMode:vt,waferCenter:t.center,waferRadius:t.radius,notchDir:Lt,binCounts:et,metadataCounts:nt,metadataColorMap:at,activeMetadataKey:p,metadataFields:$t,specCounts:ut,passFailCounts:ft,dieBounds:Jt}}export{getDieKey}from"../core/dies.js";
1
+ import{hasPosition as ie}from"../core/dies.js";import{getReticleCell as se}from"../core/reticle.js";import{metadataCategoricalValue as oe,metadataDisplayValue as re}from"../core/metadata.js";import{affineRotation as mt,affineMirror as Lt,affineCompose as it,affinePoint as Y,affineVector as pt,affineSwapsAxes as le}from"../core/transforms.js";import{contrastTextColor as G,SPEC_PASS_FILL as yt,SPEC_FAIL_LOW as ce,SPEC_FAIL_HIGH as gt}from"./colorMap.js";import{getValueColorScheme as ue}from"./colorSchemes.js";import{resolveBinColors as fe,binColorsCover as de}from"./binColors.js";import{NO_DATA_FILL as j}from"./colorMap.js";import{diePassStatus as he}from"../core/dies.js";import{getDieTestValue as L,getTestPassStatus as Q,isParametricTest as bt}from"./buildWaferMap.js";import{fmt as xt,fmtColorbarAxis as Mt,fmtAggregationMethod as Pt}from"./fmt.js";import{metadataValueColor as me}from"./colorMap.js";import{compareNatural as pe,clamp01 as Rt}from"../core/utils.js";import{prettyKey as Nt}from"../core/utils.js";export function resolveTestNumber(t,e){return e?.length?{testNumber:(findTestDef(e,t)??e[0]).testNumber}:{testNumber:t}}export function collectMetadataValues(t,e){const a=new Set;for(const n of t){if(n.partial||n.edgeExcluded)continue;const s=st(n,e);s!==void 0&&a.add(s)}return[...a].sort(pe)}export function findTestDef(t,e){return t?.find(a=>a.testNumber===e)}export function getUniqueTestNumbers(t){return[...new Set(t.flatMap(e=>e.testValues?Object.keys(e.testValues).map(Number):[]))].sort((e,a)=>e-a)}const ye="#d3d6db",Ot="#e8e9ea",ge="#aab0ba";function st(t,e){return e?oe(t.metadata?.[e]):void 0}export function classifySpec(t,e){return t===void 0?null:e?.limitLow!==void 0&&t<e.limitLow?"failLow":e?.limitHigh!==void 0&&t>e.limitHigh?"failHigh":"pass"}function be(t,e,a){const{x:n,y:s}=t.center,i=mt(t.orientation,n,s),o=it(Lt(e?.x??!1,e?.y??!1,n,s),mt(t.orientation,n,s)),l=it(Lt(a?.flipX??!1,a?.flipY??!1,n,s),mt(a?.rotation??0,n,s));return{bakedToScreen:l,physicalToScreen:it(l,i),gridToScreen:it(l,o)}}function z(t,e=!1){return{points:[t],closed:e}}function xe(t,e){const{center:a,radius:n,notch:s}=t;let i=a.x+n*Math.cos(e),o=a.y+n*Math.sin(e);if(!s)return{x:i,y:o};const l=Math.sqrt(n**2-(s.length/2)**2),u=s.length/2,g=i-a.x,p=o-a.y;return s.type==="bottom"&&p<-l?(o=a.y-l,i=a.x+Math.max(-u,Math.min(u,g))):s.type==="top"&&p>l?(o=a.y+l,i=a.x+Math.max(-u,Math.min(u,g))):s.type==="left"&&g<-l?(i=a.x-l,o=a.y+Math.max(-u,Math.min(u,p))):s.type==="right"&&g>l&&(i=a.x+l,o=a.y+Math.max(-u,Math.min(u,p))),{x:i,y:o}}function Ht(t,e){return t.map(e).join(" / ")}function Me(t,e=256){const a=Array.from({length:e},(n,s)=>t(s/(e-1)));return n=>a[Math.min(e-1,Math.max(0,Math.round(n*(e-1))))]}function Se(t,e){const a=Math.max(1,Math.min(t.width,t.height)),n=t.width/Math.max(e.length,1);return Math.max(8,Math.min(16,Math.round(Math.min(a*.55,n*1.8))))}function ke(t,e,a){if(e?.length){const n=e.flatMap(s=>{const i=s.testNumber;if(!bt(s)){const u=Q(t,i,s);return u===void 0?[]:[{key:i,label:s.name,value:u?"Pass":"Fail"}]}const o=L(t,i);if(o===void 0)return[];const l=t.testPass?.[i]===!1?" <i>(recorded fail)</i>":"";return[{key:i,label:s.name,value:`${xt(o,s.unit,a)}${l}`}]});if(n.length)return n}return t.testValues&&Object.keys(t.testValues).length>0?Object.entries(t.testValues).map(([n,s])=>({key:Number(n),label:`Test ${n}`,value:xt(s,void 0,a)})):t.testPass&&Object.keys(t.testPass).length>0?Object.entries(t.testPass).map(([n,s])=>({key:Number(n),label:`Test ${n}`,value:s?"Pass":"Fail"})):[]}export function buildHoverText(t,e,a={}){const{testDefs:n,hbinDefs:s,sbinDefs:i,fallbackFormat:o,aggrMethod:l,lotSize:u,activeTest:g,reticleConfig:p,metadataFields:B}=a,D=s?new Map(s.map(f=>[f.bin,f])):null,T=i?new Map(i.map(f=>[f.bin,f])):null,b=[`Die (${t.x}, ${t.y})`];if(p&&ie(t)){const f=se(t,p);b.push(`Reticle (${f.column}, ${f.row})`)}if(e==="stackedValues"){const f=L(t,0);if(f!==void 0){const d=n?.[0],h=d?.testNumber,k=d?.name??(h!=null?`Test ${h}`:"Value"),m=l?` (${l})`:"";b.push(`${k}${m}: ${xt(f,d?.unit,o)}`)}}else if(e==="stackedBins"||e==="stackedSoftBins"){const f=L(t,0),d=e==="stackedSoftBins"?t.sbin:t.hbin;if(f!==void 0){const h=e==="stackedSoftBins"?T:D,k=d!==void 0?h?.get(d)?.name?`${d} \xB7 ${h.get(d).name}`:`Bin ${d}`:"Bin";let m;if(l==="percent")m=`${f.toFixed(0)}%`;else{const C=u?` (${(f/u*100).toFixed(0)}% of stacked wafers)`:"";m=`${f}${C}`}const x=l?` [${Pt(l)}]`:"";b.push(`${k}: ${m}${x}`)}}else{const f=ke(t,n,o);if(e==="value"&&f.length){const{testNumber:d}=resolveTestNumber(g??0,n),h=findTestDef(n,d),k=L(t,d);let m=f.findIndex(y=>y.key===d);m<0&&(m=0);const x=f[m];let C=`<b>${x.label}: ${x.value}</b>`;if(x.key===d&&h){const y=classifySpec(k,h);(y==="failLow"||y==="failHigh")&&(C+=" <i>(out of spec)</i>")}b.push(C);const R=f.length-1;R>0&&b.push(`<i>+${R} more test${R===1?"":"s"}</i>`)}else if(f.length){const d=f.length;b.push(`<i>${d} test result${d===1?"":"s"} recorded</i>`)}if(t.hbin!==void 0||t.sbin!==void 0){const d=[];if(t.hbin!==void 0){const h=D?.get(t.hbin)?.name,k=h?`${t.hbin} \xB7 ${h}`:String(t.hbin);d.push(`HBin: ${k}`)}if(t.sbin!==void 0){const h=T?.get(t.sbin)?.name,k=h?`${t.sbin} \xB7 ${h}`:String(t.sbin);d.push(`SBin: ${k}`)}b.push(d.join(" &nbsp;\xB7&nbsp; "))}}t.retestCount!==void 0&&b.push(`Retests: ${t.retestCount}`),t.siteNum!==void 0&&b.push(`Site: ${t.siteNum}`),t.partId!==void 0&&b.push(`Part ID: ${t.partId}`),t.partial&&b.push("<i>partial die</i>"),t.probeIndex!==void 0&&b.push(`Probe: #${t.probeIndex}`);const $={...t.metadata??{}};for(const[f,d]of Object.entries($)){const h=re(d);if(h===void 0)continue;const k=B?.find(m=>m.key===f)?.label??Nt(f);b.push(`${k}: ${h}`)}return b.join("<br>")}export function buildMapTitle(t,e="engineering",a){const n=t.isLotStack&&t.lotSize!==void 0?`stacked (${t.lotSize} ${t.lotSize===1?"wafer":"wafers"})`:"stacked",s=i=>{const o=a?.length===1?a[0]:void 0;return o===void 0?`${i} Bin`:o.name?`${i} Bin ${o.bin} \xB7 ${o.name}`:`${i} Bin ${o.bin}`};switch(t.plotMode){case"hardBin":return{primary:"Hard Bin",secondary:""};case"softBin":return{primary:"Soft Bin",secondary:""};case"stackedBins":return{primary:s("Hard"),secondary:n};case"stackedSoftBins":return{primary:s("Soft"),secondary:n};case"metadata":{const i=t.metadataFields?.find(l=>l.key===t.activeMetadataKey),o=t.activeMetadataKey?Nt(t.activeMetadataKey):"Metadata";return{primary:i?.label??o,secondary:""}}case"stackedValues":{const i=t.testDefs?.[0],o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=Mt(o,i?.name,i?.unit,e),u=l||"Value",g=t.aggrMethod?` \xB7 ${Pt(t.aggrMethod)}`:"";return{primary:`${u}${g}`,secondary:n}}default:{const i=findTestDef(t.testDefs,t.activeTest),o=t.valueRange[1]||t.valueRange[0]||0,{axisLabel:l}=Mt(o,i?.name,i?.unit,e),u=l||i?.name;if(t.passFailDisplay!=="off"){const g=i?.testNumber??t.activeTest,p=u?`${u} \xB7 #${g}`:`Test ${g}`,B=t.passFailDisplay==="spec"?"Spec pass/fail":i!==void 0&&!bt(i)?"Functional pass/fail":"Tester pass/fail";return{primary:p,secondary:B}}return{primary:u??`Test ${t.activeTest}`,secondary:""}}}}export function generateTextOverlay(t,e,a){const{plotMode:n,colorFns:s,normalize:i,activeTest:o,valueRange:l,testDefs:u,fallbackFormat:g,passFailDisplay:p="off",activeMetadataKey:B,metadataColorMap:D,allIntegerValues:T}=a,b=findTestDef(u,o),{tickFmt:$}=Mt(l[1],b?.name,b?.unit,g),f=T?h=>String(Math.round(h)):$,{testNumber:d}=resolveTestNumber(o,u);return t.flatMap((h,k)=>{let m="",x="#111111";if(n==="value"&&p==="test"){const y=Q(h,d,b);if(y===void 0)return[];m=y?"P":"F",x=G(y?yt:gt)}else if(n==="value"){const y=L(h,d);if(y===void 0)return[];m=Ht([y],f),x=G(s.forValue(i(y)))}else if(n==="hardBin"||n==="softBin"){const y=n==="softBin"?h.sbin:h.hbin;if(y===void 0)return[];m=String(y),x=G(s.forBin(y))}else if(n==="metadata"){const y=st(h,B);if(y===void 0)return[];m=y,x=G(D?.get(m)??j)}else{const y=L(h,0);if(y===void 0)return[];m=Ht([y],f),x=G(s.forValue(i(y)))}const C=e?e[k*2]:h.physX,R=e?e[k*2+1]:h.physY;return[{x:C,y:R,text:m,fontSize:Se(h,m),color:x,align:"center"}]})}function At(t){return t==="top"?Math.PI/2:t==="bottom"?-Math.PI/2:t==="left"?Math.PI:0}function ve(t,e,a=720){const{center:n,radius:s,notch:i}=t,o=$=>Y(e,$.x,$.y);if(!(i!==void 0&&t.diameter>150)){const $=[];for(let f=0;f<=a;f++){const d=2*Math.PI*f/a;$.push(o(xe(t,d)))}return[{kind:"wafer-boundary",...z($,!0),lineColor:"#888888",lineWidth:1}]}const u=1.25,g=i.length,p=At(i.type),B=Math.atan2(g,s),D=s-u,T=[];let b=!1;for(let $=0;$<=a;$++){const f=2*Math.PI*$/a;let d=f-p;for(;d>Math.PI;)d-=2*Math.PI;for(;d<-Math.PI;)d+=2*Math.PI;if(Math.abs(d)<=B){if(!b){b=!0;const h={x:n.x+s*Math.cos(p-B),y:n.y+s*Math.sin(p-B)},k={x:n.x+D*Math.cos(p),y:n.y+D*Math.sin(p)},m={x:n.x+s*Math.cos(p+B),y:n.y+s*Math.sin(p+B)};T.push(o(h),o(k),o(m))}continue}T.push(o({x:n.x+s*Math.cos(f),y:n.y+s*Math.sin(f)}))}return[{kind:"wafer-boundary",...z(T,!0),lineColor:"#888888",lineWidth:1}]}function $e(t,e,a,n=360){const s=[],i=Math.max(1,Math.floor(a));for(let o=1;o<i;o++){const l=t.radius*o/i,u=[];for(let g=0;g<=n;g++){const p=2*Math.PI*g/n,B={x:t.center.x+l*Math.cos(p),y:t.center.y+l*Math.sin(p)};u.push(Y(e,B.x,B.y))}s.push({kind:"ring-boundary",...z(u,!0),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5})}return s}function Be(t,e,a,n){const{x:s,y:i}=t.center,o=t.radius,l=(f,d)=>Y(e,f,d),u=a-s,g=Math.sqrt(Math.max(0,o*o-u*u)),p=l(a,i-g),B=l(a,i+g),D=n-i,T=Math.sqrt(Math.max(0,o*o-D*D)),b=l(s-T,n),$=l(s+T,n);return[{kind:"quadrant-boundary",...z([p,B]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5},{kind:"quadrant-boundary",...z([b,$]),lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5}]}function Te(t,e){return t.map(a=>{const n=a.width/2,s=a.height/2;return{kind:"reticle",points:[[[a.x-n,a.y-s],[a.x+n,a.y-s],[a.x+n,a.y+s],[a.x-n,a.y+s]].map(([o,l])=>Y(e,o,l))],closed:!0,lineColor:"rgba(255,255,255,0.7)",lineWidth:1.5,fill:"rgba(0,0,0,0)"}})}function Ie(t){const e=t.filter(a=>a.probeIndex!==void 0).sort((a,n)=>(a.probeIndex??0)-(n.probeIndex??0));return e.length?[{kind:"probe-path",...z(e.map(a=>({x:a.physX,y:a.physY}))),lineColor:"rgba(220,80,0,0.55)",lineWidth:1}]:[]}function Ve(t,e,a,n,s,i,o,l,u,g,p,B,D,T,b="off",$,f,d){const h=e.width-o,k=e.height-o,m=i?k:h,x=i?h:k,C=S=>s==="softBin"?S.sbin:S.hbin,R=S=>st(S,$);if(e.partial){t.push({x:a,y:n,width:m,height:x,fill:ye,type:"stacked",metadata:e.metadata});return}if(e.edgeExcluded){t.push({x:a,y:n,width:m,height:x,fill:ge,type:"stacked",metadata:e.metadata});return}if(u!==void 0&&(s==="hardBin"||s==="softBin")&&C(e)!==u){t.push({x:a,y:n,width:m,height:x,fill:Ot,type:"hardBin",metadata:e.metadata});return}if(d!==void 0&&s==="metadata"&&R(e)!==d){t.push({x:a,y:n,width:m,height:x,fill:Ot,type:"metadata",metadata:e.metadata});return}if(s==="value"){if(b==="test"){const w=Q(e,p,T),ot=w===void 0?j:w?yt:gt;t.push({x:a,y:n,width:m,height:x,fill:ot,type:"value",metadata:e.metadata});return}const S=L(e,p),N=classifySpec(S,T);let _,U;S===void 0?_=j:b==="spec"?_=N==="failLow"?ce:N==="failHigh"?gt:yt:(_=l.forValue(g(S)),(N==="failLow"||N==="failHigh")&&(U=N)),t.push({x:a,y:n,width:m,height:x,fill:_,type:"value",specMark:U,metadata:e.metadata});return}if(s==="hardBin"||s==="softBin"){const S=C(e),N=S!=null?l.forBin(S):j,_=S!=null&&he(e,D)===!1?!0:void 0;t.push({x:a,y:n,width:m,height:x,fill:N,type:s,binFail:_,metadata:e.metadata});return}if(s==="metadata"){const S=R(e),N=S!==void 0?f?.get(S)??j:j;t.push({x:a,y:n,width:m,height:x,fill:N,type:"metadata",metadata:e.metadata});return}const y=L(e,0),X=y!==void 0?l.forValue(g(y)):j;t.push({x:a,y:n,width:m,height:x,fill:X,type:"value",metadata:e.metadata})}function Ce(t,e,a){const n=t.radius*.15,s=pt(e,n,0),i=pt(e,0,n),o=s.x+i.x>=0?-1:1,l=s.y+i.y>=0?-1:1,u={x:t.center.x+o*t.radius*.9,y:t.center.y+l*t.radius*.9},g={x:u.x+s.x,y:u.y+s.y},p={x:u.x+i.x,y:u.y+i.y};return a.push({x:g.x+s.x*.35,y:g.y+s.y*.35,text:"+X",fontSize:10,color:"#cc3300",align:"center",role:"indicator"},{x:p.x+i.x*.35,y:p.y+i.y*.35,text:"+Y",fontSize:10,color:"#0044cc",align:"center",role:"indicator"}),[{kind:"xy-indicator",...z([u,g]),lineColor:"#cc3300",lineWidth:2},{kind:"xy-indicator",...z([u,p]),lineColor:"#0044cc",lineWidth:2}]}export function buildView(t,e,a={},n){const s=a.reticles??[],{plotMode:i="value",showDieLabels:o=!1,showReticle:l=!1,showProbePath:u=!1,ringCount:g=4,showRingBoundaries:p=!1,showQuadrantBoundaries:B=!1,showXYIndicator:D=!1,dieGap:T=1,binColorScheme:b="default",valueColorScheme:$="default",useDefinedBinColors:f=!0,binColors:d,passBins:h=[1],highlightBin:k,highlightMetadataValue:m,interactiveTransform:x,valueRange:C,metadataValueOrder:R,testDefs:y,activeTest:X=0,activeMetadataKey:S,fallbackFormat:N="engineering",aggregationMethod:_,lotSize:U,dataAxisFlip:w,isLotStack:ot=!1,logScale:Et,colorbarRangeMode:_t="spec",passFailDisplay:wt,showPartialDies:Wt=!0}=a,St=wt??"off",rt=n?.hbinDefs,lt=n?.sbinDefs,kt=n?.metadataFields,jt=kt?.find(c=>c.key===S),zt={x:(w?.x??!1)!==(x?.flipX??!1),y:(w?.y??!1)!==(x?.flipY??!1)},Kt=rt?new Map(rt.map(c=>[c.bin,c])):null,qt=lt?new Map(lt.map(c=>[c.bin,c])):null,Xt=i==="softBin"?qt:Kt,Yt=new Set(h),ct=d&&de(d,e)?d:fe(e,{passBins:h,binColorScheme:b,hbinDefs:rt,sbinDefs:lt,useDefinedBinColors:f}),Gt=i==="softBin"?ct.soft:ct.hard,vt={forValue:Me(ue($).forValue),forBin:c=>Gt.get(c)??j};let{testNumber:H}=resolveTestNumber(X,y);if(!y?.length&&i==="value"&&!e.some(r=>r.testValues&&H in r.testValues)){const r=e.reduce((M,V)=>{if(!V.testValues)return M;const v=Object.keys(V.testValues).map(Number),P=v.length?Math.min(...v):void 0;return P!==void 0&&(M===void 0||P<M)?P:M},void 0);r!==void 0&&(H=r)}let J;if(Array.isArray(C))J=C;else if(C){const{testNumber:c}=resolveTestNumber(C.test,y);J=c===H?C.range:void 0}const I=findTestDef(y,H),$t=i==="value"&&I!==void 0&&!bt(I),Qt=I!==void 0&&(I.limitLow!==void 0||I.limitHigh!==void 0);let A="off";i==="value"&&($t?A="test":St==="spec"&&Qt?A="spec":St==="test"&&e.some(c=>Q(c,H,I)!==void 0)&&(A="test"));const Bt=A==="spec"?"spec":_t,ut=i==="stackedValues"||i==="stackedBins"||i==="stackedSoftBins";let O,E,Z=!1;if(J){[O,E]=J;let c=!0,r=!1;for(const M of e){const V=ut?L(M,0):L(M,H);if(V!==void 0&&(r=!0,!Number.isInteger(V))){c=!1;break}}Z=r&&c}else{const c=!ut&&i==="value"&&Bt==="spec"&&I&&(I.limitLow!==void 0||I.limitHigh!==void 0);let r=1/0,M=-1/0,V=!0;for(const v of e){const P=ut?L(v,0):L(v,H);P!==void 0&&(P<r&&(r=P),P>M&&(M=P),Number.isInteger(P)||(V=!1))}isFinite(r)||(V=!1),Z=V,c?(O=I.limitLow!==void 0?I.limitLow:isFinite(r)?r:0,E=I.limitHigh!==void 0?I.limitHigh:isFinite(M)?M:1):(O=isFinite(r)?r:0,E=isFinite(M)?M:1)}O===E&&(E=O+1);const Tt=$t?!1:Et??I?.logScale??!1,It=Tt&&O>0&&E>0,Ut=It;let tt;if(It){const c=Math.log10(O),r=Math.log10(E)-c;tt=M=>M<=0?0:Rt((Math.log10(M)-c)/r)}else tt=c=>Rt((c-O)/(E-O));const F=be(t,w,x);let Vt=null;if(t.notch){const c=At(t.notch.type),r=pt(F.physicalToScreen,Math.cos(c)*t.radius,Math.sin(c)*t.radius),M=Math.hypot(r.x,r.y);M>0&&(Vt={x:r.x/M,y:r.y/M})}let K=1/0;for(let c=0;c<e.length;c++){const r=e[c];r.width<K&&(K=r.width),r.height<K&&(K=r.height)}const Jt=K!==1/0?Math.min(T,K*.12):T,Ct=[],ft=[],Zt=F.bakedToScreen.a===1&&F.bakedToScreen.b===0&&F.bakedToScreen.c===0&&F.bakedToScreen.d===1&&F.bakedToScreen.e===0&&F.bakedToScreen.f===0;let W=null;if(!Zt){W=new Float64Array(e.length*2);for(let c=0;c<e.length;c++){const r=Y(F.bakedToScreen,e[c].physX,e[c].physY);W[c*2]=r.x,W[c*2+1]=r.y}}const te=le(F.gridToScreen),ee=i==="hardBin"||i==="softBin"||i==="stackedBins"||i==="stackedSoftBins",ne=i==="softBin"||i==="stackedSoftBins";let et=ee?new Map:void 0;const dt=i==="value"&&A==="spec"?{pass:0,failHigh:0,failLow:0}:void 0,ht=i==="value"&&A==="test"?{pass:0,fail:0}:void 0,Ft=i==="metadata";let nt=Ft?new Map:void 0,at=null;if(Ft&&S){const c=collectMetadataValues(e,S),r=R?.key===S?R.values:void 0,M=r?[...r,...c.filter(V=>!r.includes(V))]:c;at=new Map(M.map((V,v)=>[V,jt?.values?.find(P=>P.value===V)?.color??me(v)]))}for(let c=0;c<e.length;c++){const r=e[c],M=W?W[c*2]:r.physX,V=W?W[c*2+1]:r.physY;if(ft.push({x:M,y:V}),et&&!r.partial&&!r.edgeExcluded){const v=ne?r.sbin:r.hbin;v!=null&&et.set(v,(et.get(v)??0)+1)}if(dt&&!r.partial&&!r.edgeExcluded){const v=classifySpec(L(r,H),I);v&&dt[v]++}if(ht&&!r.partial&&!r.edgeExcluded){const v=Q(r,H,I);v!==void 0&&ht[v?"pass":"fail"]++}if(nt&&!r.partial&&!r.edgeExcluded){const v=st(r,S);v!==void 0&&nt.set(v,(nt.get(v)??0)+1)}r.partial&&!Wt||Ve(Ct,r,M,V,i,te,Jt,vt,k,tt,H,Xt,Yt,I,A,S,at,m)}const Dt=o?generateTextOverlay(e,W,{plotMode:i,colorFns:vt,normalize:tt,activeTest:X,valueRange:[O,E],testDefs:y,fallbackFormat:N,passFailDisplay:A,activeMetadataKey:S,metadataColorMap:at,allIntegerValues:Z}):[],q=ve(t,F.physicalToScreen);p&&q.push(...$e(t,F.physicalToScreen,g)),B&&q.push(...Be(t,F.physicalToScreen,t.center.x,t.center.y)),l&&q.push(...Te(s,F.gridToScreen)),u&&q.push(...Ie(e)),D&&q.push(...Ce(t,F.gridToScreen,Dt));const ae=ft.length>0?{minX:t.center.x-t.radius,maxX:t.center.x+t.radius,minY:t.center.y-t.radius,maxY:t.center.y+t.radius}:null;return{rectangles:Ct,hoverPoints:ft,texts:Dt,overlays:q,plotMode:i,binColorScheme:b,valueColorScheme:$,binColors:ct,metadata:t.metadata??null,dies:e,valueRange:[O,E],allIntegerValues:Z,testDefs:y,activeTest:X,logScale:Ut,logScaleRequested:Tt,aggrMethod:_,lotSize:U,axisFlip:zt,dataAxisFlip:{x:w?.x??!1,y:w?.y??!1},gridToScreen:F.gridToScreen,rotation:((t.orientation+(x?.rotation??0))%360+360)%360,hasReticle:s.length>0,isLotStack:ot,passFailDisplay:A,colorBySpec:A==="spec",colorbarRangeMode:Bt,waferCenter:t.center,waferRadius:t.radius,notchDir:Vt,binCounts:et,metadataCounts:nt,metadataColorMap:at,activeMetadataKey:S,metadataFields:kt,specCounts:dt,passFailCounts:ht,dieBounds:ae}}export{getDieKey}from"../core/dies.js";
@@ -252,8 +252,10 @@ export interface BinDef {
252
252
  /** Human-readable bin name, e.g. `"Pass"` or `"Contact Open"`. */
253
253
  name: string;
254
254
  /**
255
- * Optional CSS color override for this bin.
256
- * When set, overrides the active colour scheme for this bin value.
255
+ * Optional CSS colour for this bin — e.g. a site's standard bin colour sheet.
256
+ * Wins over the bin colour scheme for this bin (unless the viewer turns
257
+ * "Use colours from bin definitions" off); every bin without one still takes
258
+ * a palette colour. See `resolveBinColors`.
257
259
  */
258
260
  color?: string;
259
261
  }
@@ -299,6 +301,21 @@ export interface WaferMapInputBase {
299
301
  * Set to an empty array to suppress yield calculation.
300
302
  */
301
303
  passBins?: number[];
304
+ /**
305
+ * Wafer diameters (mm) treated as standard when sanity-checking an *inferred*
306
+ * diameter — see {@link STANDARD_WAFER_DIAMETERS_MM} for the default
307
+ * (100/125/150/200/300) and the measurements behind it.
308
+ *
309
+ * **Replaces** the default rather than adding to it, so spread it to extend:
310
+ * `standardDiameters: [...STANDARD_WAFER_DIAMETERS_MM, 76.2]` for a line that
311
+ * also runs 3-inch. Pass `[]` to disable the check entirely, for genuinely
312
+ * non-standard substrates (panels, reclaim, odd R&D shapes) — that is the
313
+ * intended opt-out, rather than suppressing every geometry advisory.
314
+ *
315
+ * Only consulted when the diameter was inferred; a diameter you supply is
316
+ * never second-guessed.
317
+ */
318
+ standardDiameters?: number[];
302
319
  /**
303
320
  * How to handle multiple `DieResult` entries for the same die position (retests).
304
321
  *
@@ -420,22 +437,47 @@ export interface WaferWarning {
420
437
  * you asserted — it reports this so you can correct one of them. Only raised
421
438
  * when the pitch was supplied: pitch is a free scaling parameter, so with an
422
439
  * inferred pitch "the dies don't fit" is not a statement about your data.
423
- * - `'inferred-pitch'` — `waferConfig.diameter` was supplied without a die pitch,
424
- * so the pitch was derived as `diameter ÷ grid span`, assuming the grid spans
425
- * the full wafer. That assumption fails whenever edge dies are absent, which
426
- * silently scales every die position. Supply `dieConfig.width`/`height`.
440
+ * - `'non-standard-diameter'` — the wafer diameter was INFERRED from the die
441
+ * extent (a pitch was supplied, a diameter was not) and came out off the
442
+ * standard wafer-size ladder (SEMI M1: 100/150/200/300 mm and the smaller
443
+ * legacy sizes). Silicon only comes in those sizes, so a result like 210 mm
444
+ * is evidence the inference went wrong — almost always because the outer
445
+ * dies were never probed, so the die extent understates the wafer. Every die
446
+ * is then placed against a wafer that is too small, which moves dies between
447
+ * rings and changes ring and edge findings. Supply `waferConfig.diameter`.
448
+ *
449
+ * There is deliberately no matching advisory for an inferred *pitch*: that
450
+ * is derived to fit the supplied diameter, so it is always self-consistent
451
+ * and there is nothing to check it against.
452
+ * - `'diameter-exceeds-die-extent'` — a SUPPLIED `waferConfig.diameter` that the
453
+ * probed dies fill less than 75% of. The mirror of `geometry-conflict`, which
454
+ * asks whether the dies *fit*; this asks whether they *fill*. An over-large
455
+ * wafer is not harmless: ring bands are equal-radius, so it crushes dies into
456
+ * the inner rings and empties the outer ones — at a 10× diameter every die
457
+ * lands in ring 1 — and ring/quadrant/edge findings then describe the assumed
458
+ * wafer, not the probed area. A genuinely partial map looks identical and is
459
+ * equally worth reporting, so the message names both causes.
427
460
  * - `'test-count-capped'` — raised by `analyzeWaferMap`: more tests were found in
428
461
  * the die data than the analysis cap allows, so test-value analysis was skipped
429
462
  * entirely and NO test findings were produced. Pass `testNumbers` to scope it.
463
+ * - `'analysis-option-corrected'` — raised by `analyzeWaferMap`: a numeric option
464
+ * was outside the range that can produce a meaningful analysis and was clamped.
465
+ * The message names each correction. Only reachable from untyped callers now
466
+ * that the statistical thresholds are internal, but a wrong `ringCount` or
467
+ * `sectorCount` still gets here from TypeScript.
430
468
  * - `'edge-exclusion-exceeds-radius'` — `waferConfig.edgeExclusion` is larger than
431
469
  * the resolved wafer radius (most likely when the diameter was itself inferred
432
470
  * from sparse/partial data). The excluded band is clamped to the whole wafer
433
471
  * rather than silently producing a smaller, wrong ring.
472
+ * - `'bin-colors-shared'` — raised by the renderers for the bin map on screen:
473
+ * some bins are drawn in the same colour as another bin (more bins than the
474
+ * bin colour scheme has distinct colours, or a `BinDef.color` repeats one).
475
+ * Every die is drawn correctly; colour alone just cannot separate those bins.
434
476
  *
435
477
  * The union is intentionally open to string so future advisory codes can be
436
478
  * added without a breaking change; switch with a `default` branch.
437
479
  */
438
- code: 'partial-coverage' | 'geometry-conflict' | 'inferred-pitch' | 'test-count-capped' | 'edge-exclusion-exceeds-radius' | (string & {});
480
+ code: 'partial-coverage' | 'geometry-conflict' | 'non-standard-diameter' | 'diameter-exceeds-die-extent' | 'test-count-capped' | 'edge-exclusion-exceeds-radius' | 'analysis-option-corrected' | (string & {});
439
481
  /** Human-readable explanation, suitable for direct display. */
440
482
  message: string;
441
483
  /**
@@ -453,7 +495,14 @@ export interface WaferWarning {
453
495
  confidence?: number;
454
496
  }
455
497
  export interface WaferMapResult {
498
+ /** Resolved wafer geometry — diameter, radius, centre, notch, orientation. */
456
499
  wafer: Wafer;
500
+ /**
501
+ * Every die, carrying its original grid coordinates (`x`/`y`), physical
502
+ * position, bin/test data and eligibility flags. Dies with no reported
503
+ * position are included here — guard with `hasPosition` before using
504
+ * coordinates.
505
+ */
457
506
  dies: Die[];
458
507
  /**
459
508
  * The initial plot mode selected by `buildWaferMap` — `'value'` when test values are
@@ -468,6 +517,17 @@ export interface WaferMapResult {
468
517
  * @internal Renderer-agnostic draw list consumed by `renderWaferMap` and `toCanvas`.
469
518
  * Not part of the public API — access the named fields on `WaferMapResult` instead.
470
519
  */
520
+ /**
521
+ * @internal The initial draw list. Kept on the result because `analyzeWaferMap`
522
+ * discriminates its input on `'view' in input`, and because `renderWaferMap`
523
+ * reads `dataAxisFlip` from it — not because a host should.
524
+ *
525
+ * Read the promoted top-level fields instead (`plotMode`, `metadata`,
526
+ * `isLotStack`, `hbinDefs`, `sbinDefs`, `testDefs`): this one is rebuilt on
527
+ * every option change, so anything you cache from it goes stale immediately.
528
+ * Hosts driving the low-level pipeline should call `buildView` themselves,
529
+ * which is the supported way to hold a {@link View}.
530
+ */
471
531
  view: View;
472
532
  /** Reticle configuration used to generate the overlay and reticle-local groupings. */
473
533
  reticleConfig?: ReticleConfig;
@@ -479,15 +539,26 @@ export interface WaferMapResult {
479
539
  * proportionally correct but not in physical mm.
480
540
  */
481
541
  units: 'mm' | 'normalized';
542
+ /**
543
+ * How much of the geometry had to be guessed, and how confident each guess is.
544
+ * `confidence` runs 0–1, where 1 means the value was supplied rather than
545
+ * inferred. Worth surfacing when a map looks wrong: a low `wafer` or `diePitch`
546
+ * confidence usually means sparse or partial data, and is the first thing to
547
+ * check before suspecting the data itself. See `warnings` for the advisories
548
+ * these confidences drive.
549
+ */
482
550
  inference: {
551
+ /** Wafer diameter/centre. `method` names how it was derived, e.g. `'inferred-from-xy'`. */
483
552
  wafer: {
484
553
  confidence: number;
485
554
  method: string;
486
555
  };
556
+ /** Die pitch. `units` is `'mm'` when a real physical scale was established, `'normalized'` when not. */
487
557
  diePitch: {
488
558
  confidence: number;
489
559
  units: 'mm' | 'normalized';
490
560
  };
561
+ /** The die grid's origin and step. */
491
562
  grid: {
492
563
  confidence: number;
493
564
  };
@@ -574,5 +645,39 @@ export declare function getTestPassStatus(die: Pick<Die, 'testValues' | 'testPas
574
645
  * availability checks.
575
646
  */
576
647
  export declare function dieHasTestData(die: Pick<Die, 'testValues' | 'testPass'>): boolean;
648
+ /**
649
+ * Nominal wafer diameters in mm, used to sanity-check an *inferred* diameter.
650
+ * Override per build with {@link WaferMapInputBase.standardDiameters}.
651
+ *
652
+ * This is what makes an inferred diameter checkable at all. When a die pitch is
653
+ * supplied but no diameter, the wafer is sized from the die extent — right only
654
+ * if the probed grid reaches the wafer edge, and silently wrong whenever the
655
+ * outer dies were never probed. Silicon comes in standard sizes, so landing off
656
+ * the ladder is evidence the assumption failed. (The library already leans on
657
+ * SEMI M1 for notch chord lengths — see `standardNotchLength` in core/wafer.ts.)
658
+ *
659
+ * **The size of this list is a real trade-off, measured, not guessed.** Every
660
+ * entry is one more value a wrong inference can hide behind; every omission
661
+ * wrongly accuses someone actually running that size. Across 63 fixtures
662
+ * (76.2–300 mm, square and rectangular die, 50–94% coverage):
663
+ *
664
+ * | table | caught | missed | false alarms |
665
+ * |---|---|---|---|
666
+ * | 100/150/200/300 | 31 | 8 | 3 |
667
+ * | **100/125/150/200/300 (this default)** | 31 | 8 | 3 |
668
+ * | + 76.2 | 28 | 11 | 0 |
669
+ * | + 25.4/50.8/450 | 26 | 13 | 0 |
670
+ *
671
+ * A miss is silent, which is what every one of these cases did before this check
672
+ * existed — so it costs nothing against the status quo. A false alarm is a new
673
+ * harm: it tells someone whose diameter is correct that it is suspect, with no
674
+ * way to clear it. Hosts running 3-inch or other legacy sizes should add them
675
+ * via `standardDiameters` rather than live with that.
676
+ *
677
+ * 25.4/50.8 (1"/2") and 450 mm are excluded: none are in current production
678
+ * flows, and including 50.8 demonstrably created a miss — a 76.2 mm wafer
679
+ * mis-inferring to 50 mm went undetected.
680
+ */
681
+ export declare const STANDARD_WAFER_DIAMETERS_MM: readonly number[];
577
682
  export declare function buildWaferMap(input: DieResult[] | WaferMapInput, options?: WaferMapOptions): WaferMapResult;
578
683
  //# sourceMappingURL=buildWaferMap.d.ts.map
@@ -1 +1 @@
1
- import{createWafer as j}from"../core/wafer.js";import{isYieldEligibleDie as fe,getDieKey as B,hasPosition as k}from"../core/dies.js";import{applyOrientation as H,transformDies as de}from"../core/transforms.js";import{affineRotation as ce,affineMirror as le,affineCompose as ue,affinePoint as he}from"../core/transforms.js";import{inferWaferFromXY as pe}from"../core/inference/wafer.js";import{resolveGridPitch as me}from"../core/inference/pitch.js";import{assignGridIndices as ye}from"../core/inference/grid.js";import{generateReticleGrid as ge}from"../core/reticle.js";import{buildView as U}from"./buildView.js";import{modeOf as xe}from"../core/utils.js";import{aggregateValues as q,aggregateBinCounts as we}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function be(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],testDefs:t.testDefs,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function Me(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function De(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let u=1/0,c=-1/0,a=1/0,l=-1/0;for(const s of t)s.x<u&&(u=s.x),s.x>c&&(c=s.x),s.y<a&&(a=s.y),s.y>l&&(l=s.y);return{offsetX:Math.round((c+u)/2),offsetY:Math.round((l+a)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function Oe(t,n,e,u,c,a){if(a)return{colMidX:(Math.round(a.x)-n)*u,colMidY:(Math.round(a.y)-e)*c,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let l=1/0,s=-1/0,h=1/0,p=-1/0;for(const i of t){const d=Math.round(i.x)-n,o=Math.round(i.y)-e;d<l&&(l=d),d>s&&(s=d),o<h&&(h=o),o>p&&(p=o)}return{colMidX:(l+s)/2*u,colMidY:(h+p)/2*c,anchored:!1}}function Se(t,n,e){let u=1/0,c=-1/0,a=1/0,l=-1/0,s=0,h=0;for(const d of t){const o=Math.round(d.x)-n,r=Math.round(d.y)-e;s+=o,h+=r,o<u&&(u=o),o>c&&(c=o),r<a&&(a=r),r>l&&(l=r)}const p=t.length,i=(d,o,r)=>{const m=(r-o)/2;return m<=0?0:Math.abs(d/p-(o+r)/2)/m};return i(s,u,c)>.11||i(h,a,l)>.11}function ke(t,n){let e=t?.xAxisDirection==="left",u=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(u=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:u}}function ve(t,n){const{method:e,targetBin:u}=t,c=t.results.map(a=>a.filter(k));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const a=new Set((n??[]).filter(i=>!isParametricTest(i)).map(i=>i.testNumber)),l=new Set;for(const i of c)for(const d of i)if(d.testValues)for(const o of Object.keys(d.testValues)){const r=Number(o);a.has(r)||l.add(r)}if(l.size===0)return q(c,e);const s=[...l],h=s.map(i=>q(c,e,i)),p=new Map;for(let i=0;i<s.length;i++){const d=s[i];for(const o of h[i]){const r=B(o);p.has(r)||p.set(r,{template:o,testValues:{}});const m=p.get(r),y=o.testValues?.[0];y!==void 0&&(m.testValues[d]=y)}}return[...p.values()].map(({template:i,testValues:d})=>({...i,testValues:Object.keys(d).length>0?d:void 0}))}if(e==="countBin"||e==="percent"){if(u===void 0)return[];const a=we(c,u,"hard");if(e==="countBin")return a;const l=c.length;return a.map(s=>({...s,testValues:{0:l>0?(s.testValues?.[0]??0)/l*100:0}}))}if(e==="mode"){const a=new Map;for(const s of c)for(const h of s){const p=B(h);a.has(p)||a.set(p,[]),a.get(p).push(h)}const l=[];for(const[s,h]of a){const p=s.split(","),i=Number(p[0]),d=Number(p[1]),o=h.map(m=>m.hbin).filter(m=>m!==void 0),r=xe(o);r!==null&&l.push({x:i,y:d,hbin:r})}return l}return[]}function J(t){const n=t.filter(k),e=n.length,u=n.filter(a=>a.edgeExcluded).length,c=n.filter(a=>dieHasTestData(a)||a.hbin!==void 0||a.sbin!==void 0).length;return{filledDies:c,totalDies:e,edgeExcludedDies:u,unpositionedDies:t.length-n.length,ratio:e>0?c/e:0}}function Q(t,n,e="exclude"){const u=new Set(n),a=t.filter(r=>!r.partial).filter(r=>r.edgeExcluded).length,l=t.filter(r=>r.partial).length;let s=0,h=0,p=!1;for(const r of t){if(!fe(r))continue;const m=r.hbin??r.sbin;m!==void 0&&(p=!0,u.has(m)?s++:h++)}const i=s+h,d=p&&i>0?s/i*100:null;let o=null;if(e==="denominator-only"){const r=i+a;o=p&&r>0?s/r*100:null}return{passDies:s,failDies:h,edgeExcludedDies:a,partialDies:l,totalDies:i,yieldPercent:d,yieldPercentGross:o}}function Z(t,n,e,u,c,a=0,l=0,s=0,h=0,p=0,i=!1,d=!1){if(!t)return[];const o=t.anchorDie??{x:0,y:0},r=ge(n,{width:t.width,height:t.height,diePitchX:u,diePitchY:c,anchorDie:{x:o.x-a,y:o.y-l},gridOrigin:{x:-s,y:-h}}),m=ue(le(i,d,n.center.x,n.center.y),ce(p,n.center.x,n.center.y));return r.filter(y=>{const v=y.width/2,w=y.height/2,b=[[y.x-v,y.y-w],[y.x+v,y.y-w],[y.x+v,y.y+w],[y.x-v,y.y+w]].map(([g,M])=>he(m,g,M)),C=Math.min(...b.map(g=>g.x)),P=Math.max(...b.map(g=>g.x)),Y=Math.min(...b.map(g=>g.y)),F=Math.max(...b.map(g=>g.y));return e.some(g=>g.physX>=C&&g.physX<P&&g.physY>=Y&&g.physY<F)})}function Re(t,n,e){const c=Math.max(0,n.radius-e)**2;return t.map(a=>{const l=a.physX-n.center.x,s=a.physY-n.center.y;return l*l+s*s>c?{...a,edgeExcluded:!0}:a})}function Ce(t,n,e){const u=t.filter(k),c=t.filter(i=>!k(i)),a=new Map;for(const i of u){let d=a.get(i.x);d||(d=new Map,a.set(i.x,d)),d.set(i.y,(d.get(i.y)??0)+1)}const l=new Set(e);function s(i,d){const o=i.hbin,r=d.hbin;if(o===void 0||r===void 0)return!1;const m=l.has(o),y=l.has(r);return n==="best"?y!==m?y:r<o:y!==m?m:r>o}const h=new Map;for(const i of u){const d=B(i),o=h.get(d);n==="first"&&o||(n==="best"||n==="worst")&&o&&!s(o,i)||h.set(d,i)}return[...Array.from(h.values()).map(i=>{const o=a.get(i.x)?.get(i.y)??1;return o>1?{...i,retestCount:o}:i}),...c]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const u=t.testPass?.[n];if(u!==void 0)return u;if(e!==void 0&&!isParametricTest(e)){const c=t.testValues?.[n];if(c===0||c===1)return c===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function T(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function ee(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function te(t){return(t.warnings??[]).map(n=>({code:Pe(n),message:n,severity:"error",confidence:t.wafer.confidence}))}function Pe(t){return t.includes(ne)?"geometry-conflict":t.includes(ie)?"inferred-pitch":"partial-coverage"}const ne="do not fit inside the supplied",ie="The die pitch was inferred as";export function buildWaferMap(t,n){const e=be(t),{debug:u,...c}=n??{},a=e.lotStackOpts?ve(e.lotStackOpts,e.testDefs):e.results;for(let f=0;f<Math.min(5,a.length);f++){const x=a[f];if(typeof x.x=="string"||typeof x.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const f of a)if(f.x==null!=(f.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(f.x)}, y=${String(f.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const l=Ce(a,e.retestPolicy,e.passBins),s={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}};if(e.explicitDies){let f=e.explicitDies;const x=l.filter(k),R=l.filter(A=>!k(A));if(x.length>0){const A=new Map(x.map(S=>[B(S),S]));f=f.map(S=>{const z=A.get(B(S));return z?T(S,z):S})}const V=e.waferOpts?.diameter??300,E=j({diameter:V,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});E.orientation!==0&&(f=H(f,E));const K=Z(e.reticleOpts,E,f,1,1,0,0,0,0,E.orientation),re=c.showReticle??e.reticleOpts!==void 0,L=U(E,f,{...c,reticles:K,showReticle:re,plotMode:ee(l,c),testDefs:e.testDefs,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ae=R.map((A,S)=>T({id:`unpositioned_${S}`,width:f[0]?.width??1,height:f[0]?.height??1},A)),_=[...f,...ae];return{wafer:E,dies:_,view:L,reticleConfig:e.reticleOpts,units:"mm",inference:s,warnings:te(s),plotMode:L.plotMode,metadata:L.metadata,isLotStack:!1,dataCoverage:J(_),yield:Q(_,e.passBins,e.edgeDieYieldMode),reticles:K,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const h=l.filter(k),p=l.filter(f=>!k(f)),i=h.map(f=>({x:f.x,y:f.y})),d=me(i,e.dieOpts,e.waferOpts?.diameter);s.diePitch={confidence:d.confidence,units:d.units};const{pitchX:o,pitchY:r}=d,m=d.units,y=Me(h,e.dieOpts),v=ye(i);s.grid={confidence:v.confidence};const{offsetX:w,offsetY:b}=De(i,y,v),{flipX:C,flipY:P}=ke(e.dieOpts,y),{colMidX:Y,colMidY:F,anchored:g}=Oe(i,w,b,o,r,e.waferOpts?.center);let M=e.waferOpts?.diameter;const $=i.map(f=>({x:(Math.round(f.x)-w)*o-Y,y:(Math.round(f.y)-b)*r-F})),X=$.length>0?Math.max(...$.map(({x:f,y:x})=>Math.hypot(Math.abs(f)+o/2,Math.abs(x)+r/2))):0;if(M===void 0)if(i.length>0)if(m==="mm"){const f=pe($,{minRadius:X});M=f.diameter,s.wafer={confidence:f.confidence,method:f.method}}else M=X*2,s.wafer={confidence:d.confidence*.8,method:"extent"};else M=m==="mm"?300:30,s.wafer={confidence:0,method:"default"};if(!g&&i.length>0&&Se(i,w,b)&&(s.wafer.method="inferred-partial",(s.warnings??=[]).push("Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly.")),e.waferOpts?.diameter!==void 0){const f=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(f&&X>M/2+1e-9){const x=$.filter(({x:R,y:V})=>Math.hypot(Math.abs(R)+o/2,Math.abs(V)+r/2)>M/2+1e-9).length;(s.warnings??=[]).push(`${x} of ${$.length} probed die positions ${ne} ${M} mm wafer at the supplied die pitch of ${o} \xD7 ${r} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(X*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`)}else f||(s.warnings??=[]).push(`${ie} ${o.toFixed(3)} \xD7 ${r.toFixed(3)} mm by assuming the die grid fits within the ${M} mm wafer. The assumed aspect ratio may not match the true die shape whenever edge dies are absent from the data (a reticle-complete map, or partial dies filtered out upstream). Supply dieConfig.width and dieConfig.height \u2014 the die pitch, not the diameter, is what fixes placement.`)}const O=j({diameter:M,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata}),Ee={width:o,height:r};let D=h.map(f=>{const x=Math.round(f.x)-w,R=Math.round(f.y)-b,V={id:`${x}_${R}`,x,y:R,physX:x*o-Y,physY:R*r-F,width:o,height:r,insideWafer:!0,partial:!1};return T(V,f)});(w!==0||b!==0)&&(D=D.map(f=>({...f,x:f.x+w,y:f.y+b,id:`${f.x+w}_${f.y+b}`}))),D=H(D,O),(C||P)&&(D=de(D,{flipX:C,flipY:P},O.center));const N=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(D=Re(D,O,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>O.radius&&N.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${O.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const G=Z(e.reticleOpts,O,D,o,r,w,b,Y,F,O.orientation,C,P),se=c.showReticle??e.reticleOpts!==void 0,I=U(O,D,{...c,reticles:G,showReticle:se,plotMode:ee(l,c),testDefs:e.testDefs,dataAxisFlip:{x:C,y:P},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),oe=p.map((f,x)=>T({id:`unpositioned_${x}`,width:o,height:r},f)),W=[...D,...oe];return{wafer:O,dies:W,view:I,reticleConfig:e.reticleOpts,units:m,inference:s,warnings:[...te(s),...N],plotMode:I.plotMode,metadata:I.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:J(W),yield:Q(W,e.passBins,e.edgeDieYieldMode),reticles:G,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:I.aggrMethod,lotSize:I.lotSize}}
1
+ import{createWafer as j}from"../core/wafer.js";import{isYieldEligibleDie as ce,getDieKey as V,hasPosition as E}from"../core/dies.js";import{applyOrientation as q,transformDies as le}from"../core/transforms.js";import{affineRotation as ue,affineMirror as he,affineCompose as pe,affinePoint as me}from"../core/transforms.js";import{inferWaferFromXY as ge}from"../core/inference/wafer.js";import{resolveGridPitch as ye}from"../core/inference/pitch.js";import{assignGridIndices as we}from"../core/inference/grid.js";import{generateReticleGrid as xe}from"../core/reticle.js";import{buildView as H}from"./buildView.js";import{modeOf as De}from"../core/utils.js";import{aggregateValues as U,aggregateBinCounts as be}from"../core/aggregates.js";export function isParametricTest(t){return t?.testType!=="F"}function Me(t){if(!Array.isArray(t)&&"results"in t&&t.results!==void 0&&"lotStack"in t&&t.lotStack!==void 0)throw new Error("buildWaferMap: pass either `results` or `lotStack`, not both.");return Array.isArray(t)?{results:t,waferOpts:void 0,dieOpts:void 0,explicitDies:void 0,reticleOpts:void 0,lotStackOpts:void 0,passBins:[1],standardDiameters:void 0,testDefs:void 0,hbinDefs:void 0,sbinDefs:void 0,metadataFields:void 0,retestPolicy:"last",edgeDieYieldMode:"exclude"}:{results:t.results??[],waferOpts:t.waferConfig,dieOpts:t.dieConfig,explicitDies:t.dies,reticleOpts:t.reticleConfig,lotStackOpts:t.lotStack,passBins:t.passBins??[1],standardDiameters:t.standardDiameters,testDefs:t.testDefs?.length?t.testDefs:void 0,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,metadataFields:t.metadataFields,retestPolicy:t.retestPolicy??"last",edgeDieYieldMode:t.edgeDieYieldMode??"exclude"}}function Oe(t,n){return n?.coordinateOrigin?n.coordinateOrigin:{type:"center"}}function Re(t,n,e){if(n.type==="custom"&&n.offset)return{offsetX:n.offset.x,offsetY:n.offset.y};if(n.type!=="center"){let u=1/0,c=-1/0,a=1/0,l=-1/0;for(const s of t)s.x<u&&(u=s.x),s.x>c&&(c=s.x),s.y<a&&(a=s.y),s.y>l&&(l=s.y);return{offsetX:Math.round((c+u)/2),offsetY:Math.round((l+a)/2)}}return{offsetX:e.offsetX,offsetY:e.offsetY}}function Se(t,n,e,u,c,a){if(a)return{colMidX:(Math.round(a.x)-n)*u,colMidY:(Math.round(a.y)-e)*c,anchored:!0};if(t.length===0)return{colMidX:0,colMidY:0,anchored:!1};let l=1/0,s=-1/0,h=1/0,p=-1/0;for(const i of t){const f=Math.round(i.x)-n,r=Math.round(i.y)-e;f<l&&(l=f),f>s&&(s=f),r<h&&(h=r),r>p&&(p=r)}return{colMidX:(l+s)/2*u,colMidY:(h+p)/2*c,anchored:!1}}function Ee(t,n,e){let u=1/0,c=-1/0,a=1/0,l=-1/0,s=0,h=0;for(const f of t){const r=Math.round(f.x)-n,d=Math.round(f.y)-e;s+=r,h+=d,r<u&&(u=r),r>c&&(c=r),d<a&&(a=d),d>l&&(l=d)}const p=t.length,i=(f,r,d)=>{const m=(d-r)/2;return m<=0?0:Math.abs(f/p-(r+d)/2)/m};return i(s,u,c)>.11||i(h,a,l)>.11}function ve(t,n){let e=t?.xAxisDirection==="left",u=t?.yAxisDirection==="down";return(n.type==="UL"||n.type==="UR")&&(u=!0),(n.type==="LR"||n.type==="UR")&&(e=!0),{flipX:e,flipY:u}}function ke(t,n){const{method:e,targetBin:u}=t,c=t.results.map(a=>a.filter(E));if(e==="mean"||e==="median"||e==="stddev"||e==="min"||e==="max"||e==="count"){const a=new Set((n??[]).filter(i=>!isParametricTest(i)).map(i=>i.testNumber)),l=new Set;for(const i of c)for(const f of i)if(f.testValues)for(const r of Object.keys(f.testValues)){const d=Number(r);a.has(d)||l.add(d)}if(l.size===0)return U(c,e);const s=[...l],h=s.map(i=>U(c,e,i)),p=new Map;for(let i=0;i<s.length;i++){const f=s[i];for(const r of h[i]){const d=V(r);p.has(d)||p.set(d,{template:r,testValues:{}});const m=p.get(d),g=r.testValues?.[0];g!==void 0&&(m.testValues[f]=g)}}return[...p.values()].map(({template:i,testValues:f})=>({...i,testValues:Object.keys(f).length>0?f:void 0}))}if(e==="countBin"||e==="percent"){if(u===void 0)return[];const a=be(c,u,"hard");if(e==="countBin")return a;const l=c.length;return a.map(s=>({...s,testValues:{0:l>0?(s.testValues?.[0]??0)/l*100:0}}))}if(e==="mode"){const a=new Map;for(const s of c)for(const h of s){const p=V(h);a.has(p)||a.set(p,[]),a.get(p).push(h)}const l=[];for(const[s,h]of a){const p=s.split(","),i=Number(p[0]),f=Number(p[1]),r=h.map(m=>m.hbin).filter(m=>m!==void 0),d=De(r);d!==null&&l.push({x:i,y:f,hbin:d})}return l}return[]}function J(t){const n=t.filter(E),e=n.length,u=n.filter(a=>a.edgeExcluded).length,c=n.filter(a=>dieHasTestData(a)||a.hbin!==void 0||a.sbin!==void 0).length;return{filledDies:c,totalDies:e,edgeExcludedDies:u,unpositionedDies:t.length-n.length,ratio:e>0?c/e:0}}function Q(t,n,e="exclude"){const u=new Set(n),a=t.filter(d=>!d.partial).filter(d=>d.edgeExcluded).length,l=t.filter(d=>d.partial).length;let s=0,h=0,p=!1;for(const d of t){if(!ce(d))continue;const m=d.hbin??d.sbin;m!==void 0&&(p=!0,u.has(m)?s++:h++)}const i=s+h,f=p&&i>0?s/i*100:null;let r=null;if(e==="denominator-only"){const d=i+a;r=p&&d>0?s/d*100:null}return{passDies:s,failDies:h,edgeExcludedDies:a,partialDies:l,totalDies:i,yieldPercent:f,yieldPercentGross:r}}function Z(t,n,e,u,c,a=0,l=0,s=0,h=0,p=0,i=!1,f=!1){if(!t)return[];const r=t.anchorDie??{x:0,y:0},d=xe(n,{width:t.width,height:t.height,diePitchX:u,diePitchY:c,anchorDie:{x:r.x-a,y:r.y-l},gridOrigin:{x:-s,y:-h}}),m=pe(he(i,f,n.center.x,n.center.y),ue(p,n.center.x,n.center.y));return d.filter(g=>{const v=g.width/2,D=g.height/2,b=[[g.x-v,g.y-D],[g.x+v,g.y-D],[g.x+v,g.y+D],[g.x-v,g.y+D]].map(([w,x])=>me(m,w,x)),k=Math.min(...b.map(w=>w.x)),A=Math.max(...b.map(w=>w.x)),F=Math.min(...b.map(w=>w.y)),P=Math.max(...b.map(w=>w.y));return e.some(w=>w.physX>=k&&w.physX<A&&w.physY>=F&&w.physY<P)})}function Ae(t,n,e){const c=Math.max(0,n.radius-e)**2;return t.map(a=>{const l=a.physX-n.center.x,s=a.physY-n.center.y;return l*l+s*s>c?{...a,edgeExcluded:!0}:a})}function Ce(t,n,e){const u=t.filter(E),c=t.filter(i=>!E(i)),a=new Map;for(const i of u){let f=a.get(i.x);f||(f=new Map,a.set(i.x,f)),f.set(i.y,(f.get(i.y)??0)+1)}const l=new Set(e);function s(i,f){const r=i.hbin,d=f.hbin;if(r===void 0||d===void 0)return!1;const m=l.has(r),g=l.has(d);return n==="best"?g!==m?g:d<r:g!==m?m:d>r}const h=new Map;for(const i of u){const f=V(i),r=h.get(f);n==="first"&&r||(n==="best"||n==="worst")&&r&&!s(r,i)||h.set(f,i)}return[...Array.from(h.values()).map(i=>{const r=a.get(i.x)?.get(i.y)??1;return r>1?{...i,retestCount:r}:i}),...c]}export function getDieTestValue(t,n){return t.testValues?.[n]}export function getTestPassStatus(t,n,e){const u=t.testPass?.[n];if(u!==void 0)return u;if(e!==void 0&&!isParametricTest(e)){const c=t.testValues?.[n];if(c===0||c===1)return c===1}}export function dieHasTestData(t){return t.testValues!==void 0&&Object.keys(t.testValues).length>0||t.testPass!==void 0&&Object.keys(t.testPass).length>0}function L(t,n){const e={};return n.hbin!==void 0&&(e.hbin=n.hbin),n.sbin!==void 0&&(e.sbin=n.sbin),n.retestCount!==void 0&&(e.retestCount=n.retestCount),n.siteNum!==void 0&&(e.siteNum=n.siteNum),n.partId!==void 0&&(e.partId=n.partId),n.metadata!==void 0&&(e.metadata=n.metadata),n.testPass!==void 0&&(e.testPass=n.testPass),n.testValues!==void 0&&(e.testValues=n.testValues),{...t,...e}}function ee(t,n){return n.plotMode?n.plotMode:t.some(dieHasTestData)?"value":"hardBin"}function te(t){return(t.warnings??[]).map(n=>{const e=Ie(n);return{code:e,message:n,severity:e==="non-standard-diameter"||e==="diameter-exceeds-die-extent"?"warning":"error",confidence:t.wafer.confidence}})}function Ie(t){return t.includes(ne)?"geometry-conflict":t.includes(ie)?"non-standard-diameter":t.includes(se)?"diameter-exceeds-die-extent":"partial-coverage"}const ne="do not fit inside the supplied",ie="Inferred wafer diameter",se="The probed dies reach only",Fe=.75,Pe=20;export const STANDARD_WAFER_DIAMETERS_MM=[100,125,150,200,300];const _e=.02;function oe(t,n){return n.reduce((e,u)=>Math.abs(u-t)<Math.abs(e-t)?u:e)}function Ye(t,n){if(!n.length)return!0;const e=oe(t,n);return Math.abs(e-t)/e<=_e}function $e(t){return t===void 0?STANDARD_WAFER_DIAMETERS_MM:t.filter(n=>Number.isFinite(n)&&n>0)}export function buildWaferMap(t,n){const e=Me(t),{debug:u,...c}=n??{},a=e.lotStackOpts?ke(e.lotStackOpts,e.testDefs):e.results;for(let o=0;o<Math.min(5,a.length);o++){const y=a[o];if(typeof y.x=="string"||typeof y.y=="string")throw new TypeError("buildWaferMap: x and y must be numbers, received strings. Did you forget to cast CSV values? e.g. { x: +row.x, y: +row.y }")}for(const o of a)if(o.x==null!=(o.y==null))throw new TypeError(`buildWaferMap: die has only one of x/y set (x=${String(o.x)}, y=${String(o.y)}). A die must be either fully positioned (both x and y) or fully unpositioned (neither) \u2014 omit both to mark a die as having no reported position.`);const l=Ce(a,e.retestPolicy,e.passBins),s={wafer:{confidence:1,method:"provided"},diePitch:{confidence:1,units:"mm"},grid:{confidence:1}};if(e.explicitDies){let o=e.explicitDies;const y=l.filter(E),R=l.filter(T=>!E(T));if(y.length>0){const T=new Map(y.map(S=>[V(S),S]));o=o.map(S=>{const G=T.get(V(S));return G?L(S,G):S})}const $=e.waferOpts?.diameter??300,I=j({diameter:$,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});I.orientation!==0&&(o=q(o,I));const z=Z(e.reticleOpts,I,o,1,1,0,0,0,0,I.orientation),de=c.showReticle??e.reticleOpts!==void 0,B=H(I,o,{...c,reticles:z,showReticle:de,plotMode:ee(l,c),testDefs:e.testDefs,isLotStack:!1},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),fe=R.map((T,S)=>L({id:`unpositioned_${S}`,width:o[0]?.width??1,height:o[0]?.height??1},T)),W=[...o,...fe];return{wafer:I,dies:W,view:B,reticleConfig:e.reticleOpts,units:"mm",inference:s,warnings:te(s),plotMode:B.plotMode,metadata:B.metadata,isLotStack:!1,dataCoverage:J(W),yield:Q(W,e.passBins,e.edgeDieYieldMode),reticles:z,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields}}const h=l.filter(E),p=l.filter(o=>!E(o)),i=h.map(o=>({x:o.x,y:o.y})),f=ye(i,e.dieOpts,e.waferOpts?.diameter);s.diePitch={confidence:f.confidence,units:f.units};const{pitchX:r,pitchY:d}=f,m=f.units,g=Oe(h,e.dieOpts),v=we(i);s.grid={confidence:v.confidence};const{offsetX:D,offsetY:b}=Re(i,g,v),{flipX:k,flipY:A}=ve(e.dieOpts,g),{colMidX:F,colMidY:P,anchored:w}=Se(i,D,b,r,d,e.waferOpts?.center);let x=e.waferOpts?.diameter;const _=i.map(o=>({x:(Math.round(o.x)-D)*r-F,y:(Math.round(o.y)-b)*d-P})),C=_.length>0?Math.max(..._.map(({x:o,y})=>Math.hypot(Math.abs(o)+r/2,Math.abs(y)+d/2))):0;if(x===void 0)if(i.length>0)if(m==="mm"){const o=ge(_,{minRadius:C});x=o.diameter,s.wafer={confidence:o.confidence,method:o.method};const y=$e(e.standardDiameters);if(!Ye(x,y)){const R=oe(x,y);(s.warnings??=[]).push(`${ie} ${x.toFixed(1)} mm is not a standard wafer size (nearest is ${R} mm). The diameter was derived from the die extent, which assumes the probed grid reaches the wafer edge \u2014 a non-standard result usually means it does not, because the outer dies were never probed. Every die is then placed against a wafer that is too small, which moves dies between rings and changes ring and edge findings. Supply waferConfig.diameter.`)}}else x=C*2,s.wafer={confidence:f.confidence*.8,method:"extent"};else x=m==="mm"?300:30,s.wafer={confidence:0,method:"default"};if(!w&&i.length>0&&Ee(i,D,b)&&(s.wafer.method="inferred-partial",(s.warnings??=[]).push("Wafer geometry was inferred from die positions alone. The data does not span a full symmetric wafer, so the inferred diameter and centre may be wrong and dies may be mis-positioned relative to the true wafer boundary. Supply waferConfig.diameter and waferConfig.center (the prober coordinate of the wafer centre) to position partial data correctly.")),e.waferOpts?.diameter!==void 0&&e.waferOpts.center===void 0&&C>0&&i.length>=Pe){const o=C/(x/2);o<Fe&&(s.warnings??=[]).push(`${se} ${(o*100).toFixed(0)}% of the ${x} mm wafer's radius. Either the diameter is larger than the real one, or this is a partial map covering only part of the wafer. Ring bands are equal-radius, so the outer rings are mostly or entirely empty and ring, quadrant and edge findings describe the assumed wafer rather than the probed area. Check waferConfig.diameter, or drop it and let the diameter be inferred from the die extent.`)}if(e.waferOpts?.diameter!==void 0){const o=e.dieOpts?.width!==void 0&&e.dieOpts?.height!==void 0;if(o&&C>x/2+1e-9){const y=_.filter(({x:R,y:$})=>Math.hypot(Math.abs(R)+r/2,Math.abs($)+d/2)>x/2+1e-9).length;(s.warnings??=[]).push(`${y} of ${_.length} probed die positions ${ne} ${x} mm wafer at the supplied die pitch of ${r} \xD7 ${d} mm. A die with test results is a real prober position and is always fully on the wafer, so one of the two supplied values must be wrong: containing these dies at this pitch would need a diameter of at least ${(C*2).toFixed(1)} mm. Check waferConfig.diameter and dieConfig.width/height against the real device.`)}}const O=j({diameter:x,notch:e.waferOpts?.notch,orientation:e.waferOpts?.orientation??0,metadata:e.waferOpts?.metadata});let M=h.map(o=>{const y=Math.round(o.x)-D,R=Math.round(o.y)-b,$={id:`${y}_${R}`,x:y,y:R,physX:y*r-F,physY:R*d-P,width:r,height:d,insideWafer:!0,partial:!1};return L($,o)});(D!==0||b!==0)&&(M=M.map(o=>({...o,x:o.x+D,y:o.y+b,id:`${o.x+D}_${o.y+b}`}))),M=q(M,O),(k||A)&&(M=le(M,{flipX:k,flipY:A},O.center));const X=[];e.waferOpts?.edgeExclusion&&e.waferOpts.edgeExclusion>0&&(M=Ae(M,O,e.waferOpts.edgeExclusion),e.waferOpts.edgeExclusion>O.radius&&X.push({code:"edge-exclusion-exceeds-radius",message:`edgeExclusion (${e.waferOpts.edgeExclusion}mm) exceeds the resolved wafer radius (${O.radius}mm) \u2014 the entire wafer is excluded.`,severity:"warning"}));const K=Z(e.reticleOpts,O,M,r,d,D,b,F,P,O.orientation,k,A),re=c.showReticle??e.reticleOpts!==void 0,Y=H(O,M,{...c,reticles:K,showReticle:re,plotMode:ee(l,c),testDefs:e.testDefs,dataAxisFlip:{x:k,y:A},isLotStack:e.lotStackOpts!==void 0,aggregationMethod:e.lotStackOpts?.method,lotSize:e.lotStackOpts?.results.length},{hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,metadataFields:e.metadataFields}),ae=p.map((o,y)=>L({id:`unpositioned_${y}`,width:r,height:d},o)),N=[...M,...ae];return{wafer:O,dies:N,view:Y,reticleConfig:e.reticleOpts,units:m,inference:s,warnings:[...te(s),...X],plotMode:Y.plotMode,metadata:Y.metadata,isLotStack:e.lotStackOpts!==void 0,dataCoverage:J(N),yield:Q(N,e.passBins,e.edgeDieYieldMode),reticles:K,hbinDefs:e.hbinDefs,sbinDefs:e.sbinDefs,testDefs:e.testDefs,metadataFields:e.metadataFields,aggrMethod:Y.aggrMethod,lotSize:Y.lotSize}}
@@ -6,24 +6,29 @@ export declare const SPEC_PASS_FILL = "#2ecc71";
6
6
  export declare const SPEC_FAIL_LOW = "#3498db";
7
7
  export declare const SPEC_FAIL_HIGH = "#e74c3c";
8
8
  /**
9
- * Shared categorical palette for hard and soft bin colouring.
10
- * Index 0 is the no-data grey sentinel. Indices 1–63 are perceptually
11
- * spread colours generated via golden-angle HSL stepping.
12
- * Hard and soft bins use different hash salts so the same bin number
13
- * maps to different colours in each scheme.
9
+ * Fill for a die with no value for what is being plotted — no bin in a bin
10
+ * mode, no reading in a value mode. Shared by the map and every surface that
11
+ * draws a "no data" swatch, so the two can never disagree.
12
+ */
13
+ export declare const NO_DATA_FILL = "#d6d9dd";
14
+ /**
15
+ * Categorical fallback palette for `'metadata'` mode values beyond
16
+ * METADATA_PALETTE's fixed slots. Index 0 is a no-data grey sentinel.
17
+ * Indices 1–63 are perceptually spread colours generated via golden-angle
18
+ * HSL stepping.
19
+ *
20
+ * No longer used for bins: bin colours are rank-assigned from a registered bin
21
+ * palette by `resolveBinColors` (binColors.ts). Hashing a bin number into this
22
+ * list is what made two different bins share a colour.
14
23
  */
15
24
  export declare const BIN_PALETTE: readonly string[];
16
25
  /** Wang hash — maps any integer to a well-distributed unsigned 32-bit value. Shared with colorSchemes. */
17
26
  export declare function wangHash(n: number): number;
18
- /** Categorical colour for a hard bin. No-data handling is the caller's responsibility. */
19
- export declare function hardBinColor(bin: number): string;
20
27
  /** Linear interpolation across RGB keypoints for t ∈ [0, 1]. */
21
28
  export declare function lerpKp(kp: readonly [number, number, number][], t: number): string;
22
29
  export declare const VIRIDIS: readonly [number, number, number][];
23
30
  /** Map t ∈ [0, 1] to a Viridis RGB colour string. */
24
31
  export declare function valueToViridis(t: number): string;
25
- /** Categorical colour for a soft bin. No-data handling is the caller's responsibility. */
26
- export declare function softBinColor(bin: number): string;
27
32
  /**
28
33
  * Categorical colour for the `index`-th distinct value of an active
29
34
  * `'metadata'` field, where `index` comes from sorting the field's distinct
@@ -31,14 +36,10 @@ export declare function softBinColor(bin: number): string;
31
36
  * iteration order). Ordered assignment, not hashing, for the first
32
37
  * `METADATA_PALETTE.length` slots — this maximizes distinctness for the
33
38
  * common case of a handful of categories, unlike a hash which doesn't
34
- * optimize for a *known* small set. Falls back to the same
35
- * hash+`BIN_PALETTE` mechanism `hardBinColor`/`softBinColor` use (a new
36
- * salt) for wafers with an unusually large category count.
39
+ * optimize for a *known* small set. Falls back to hashing into `BIN_PALETTE`
40
+ * for wafers with an unusually large category count.
37
41
  */
38
42
  export declare function metadataValueColor(index: number): string;
39
- /** Categorical greyscale shades for hard bins. Index 0 = no data. */
40
- export declare const HARD_BIN_GREY: readonly string[];
41
- export declare function hardBinGreyscale(bin: number): string;
42
43
  /** Map t ∈ [0, 1] to a greyscale rgb string (range 30–230 to avoid pure black/white). */
43
44
  export declare function valueToGreyscale(t: number): string;
44
45
  /** Return '#000000' or '#ffffff' for maximum contrast against the given colour. */
@@ -1 +1 @@
1
- import{clamp01 as f}from"../core/utils.js";export const SPEC_PASS_FILL="#2ecc71",SPEC_FAIL_LOW="#3498db",SPEC_FAIL_HIGH="#e74c3c",BIN_PALETTE=["#95a5a6","#1eb84b","#932cdd","#b8a51e","#2cbfdd","#b81e71","#58dd2c","#251eb8","#dd672c","#1eb87f","#ce2cdd","#98b81e","#2c84dd","#b81e3e","#2cdd3b","#581eb8","#dda22c","#1eb8b2","#dd2cb0","#64b81e","#2c49dd","#b8321e","#2cdd76","#8c1eb8","#dcdd2c","#1e8bb8","#dd2c75","#31b81e","#4b2cdd","#b8651e","#2cddb1","#b81eb1","#a1dd2c","#1e57b8","#dd2c3a","#1eb83f","#852cdd","#b8991e","#2ccddd","#b81e7e","#66dd2c","#1e24b8","#dd5a2c","#1eb872","#c02cdd","#a4b81e","#2c92dd","#b81e4a","#2cdd2e","#4c1eb8","#dd942c","#1eb8a6","#dd2cbe","#70b81e","#2c57dd","#b8261e","#2cdd69","#801eb8","#ddcf2c","#1e97b8","#dd2c83","#3db81e","#3d2cdd","#b8591e"];export function wangHash(e){let c=e|0;return c=Math.imul(c^c>>>16,73244475),c=Math.imul(c^c>>>16,73244475),(c^c>>>16)>>>0}const s=BIN_PALETTE.length-1,i=2654435769,h=1818371886,x={1:"#2ecc71",2:"#e74c3c",3:"#f39c12",4:"#9b59b6",5:"#3498db",6:"#1abc9c",7:"#e67e22",8:"#2c3e50",9:"#c0392b",10:"#8e44ad",11:"#2980b9",12:"#27ae60",13:"#d35400",14:"#16a085"};export function hardBinColor(e){return x[e]??BIN_PALETTE[wangHash(e^i)%s+1]}export function lerpKp(e,c){const b=f(c)*(e.length-1),t=Math.floor(b),d=Math.min(t+1,e.length-1),a=b-t,n=Math.round(e[t][0]+a*(e[d][0]-e[t][0])),o=Math.round(e[t][1]+a*(e[d][1]-e[t][1])),l=Math.round(e[t][2]+a*(e[d][2]-e[t][2]));return`rgb(${n},${o},${l})`}export const VIRIDIS=[[68,1,84],[59,82,139],[33,145,140],[94,201,98],[253,231,37]];export function valueToViridis(e){return lerpKp(VIRIDIS,e)}export function softBinColor(e){return BIN_PALETTE[wangHash(e^h)%s+1]}const u=["#4e79a7","#f28e2b","#e15759","#76b7b2","#59a14f","#edc948","#b07aa1","#ff9da7","#9c755f","#bab0ac"],A=668265263;export function metadataValueColor(e){return e<u.length?u[e]:BIN_PALETTE[wangHash(e^A)%s+1]}export const HARD_BIN_GREY=["#aaaaaa","#f7f7f7","#303030","#888888","#bbbbbb","#666666","#999999","#555555","#444444","#222222","#cccccc","#777777","#eeeeee","#333333","#888888"];export function hardBinGreyscale(e){return HARD_BIN_GREY[Math.max(0,Math.min(e,HARD_BIN_GREY.length-1))]}export function valueToGreyscale(e){const c=Math.round(f(e)*200+30);return`rgb(${c},${c},${c})`}export function contrastTextColor(e){let c=0,r=0,b=0;const t=e.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);if(t)c=+t[1],r=+t[2],b=+t[3];else{const n=e.replace("#","");c=parseInt(n.slice(0,2),16),r=parseInt(n.slice(2,4),16),b=parseInt(n.slice(4,6),16)}const d=n=>{const o=n/255;return o<=.03928?o/12.92:((o+.055)/1.055)**2.4};return .2126*d(c)+.7152*d(r)+.0722*d(b)>.179?"#000000":"#ffffff"}
1
+ import{clamp01 as s}from"../core/utils.js";export const SPEC_PASS_FILL="#2ecc71",SPEC_FAIL_LOW="#3498db",SPEC_FAIL_HIGH="#e74c3c",NO_DATA_FILL="#d6d9dd",BIN_PALETTE=["#95a5a6","#1eb84b","#932cdd","#b8a51e","#2cbfdd","#b81e71","#58dd2c","#251eb8","#dd672c","#1eb87f","#ce2cdd","#98b81e","#2c84dd","#b81e3e","#2cdd3b","#581eb8","#dda22c","#1eb8b2","#dd2cb0","#64b81e","#2c49dd","#b8321e","#2cdd76","#8c1eb8","#dcdd2c","#1e8bb8","#dd2c75","#31b81e","#4b2cdd","#b8651e","#2cddb1","#b81eb1","#a1dd2c","#1e57b8","#dd2c3a","#1eb83f","#852cdd","#b8991e","#2ccddd","#b81e7e","#66dd2c","#1e24b8","#dd5a2c","#1eb872","#c02cdd","#a4b81e","#2c92dd","#b81e4a","#2cdd2e","#4c1eb8","#dd942c","#1eb8a6","#dd2cbe","#70b81e","#2c57dd","#b8261e","#2cdd69","#801eb8","#ddcf2c","#1e97b8","#dd2c83","#3db81e","#3d2cdd","#b8591e"];export function wangHash(e){let c=e|0;return c=Math.imul(c^c>>>16,73244475),c=Math.imul(c^c>>>16,73244475),(c^c>>>16)>>>0}const u=BIN_PALETTE.length-1;export function lerpKp(e,c){const n=s(c)*(e.length-1),d=Math.floor(n),t=Math.min(d+1,e.length-1),a=n-d,b=Math.round(e[d][0]+a*(e[t][0]-e[d][0])),o=Math.round(e[d][1]+a*(e[t][1]-e[d][1])),l=Math.round(e[d][2]+a*(e[t][2]-e[d][2]));return`rgb(${b},${o},${l})`}export const VIRIDIS=[[68,1,84],[59,82,139],[33,145,140],[94,201,98],[253,231,37]];export function valueToViridis(e){return lerpKp(VIRIDIS,e)}const f=["#4e79a7","#f28e2b","#e15759","#76b7b2","#59a14f","#edc948","#b07aa1","#ff9da7","#9c755f","#bab0ac"],i=668265263;export function metadataValueColor(e){return e<f.length?f[e]:BIN_PALETTE[wangHash(e^i)%u+1]}export function valueToGreyscale(e){const c=Math.round(s(e)*200+30);return`rgb(${c},${c},${c})`}export function contrastTextColor(e){let c=0,r=0,n=0;const d=e.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);if(d)c=+d[1],r=+d[2],n=+d[3];else{const b=e.replace("#","");c=parseInt(b.slice(0,2),16),r=parseInt(b.slice(2,4),16),n=parseInt(b.slice(4,6),16)}const t=b=>{const o=b/255;return o<=.03928?o/12.92:((o+.055)/1.055)**2.4};return .2126*t(c)+.7152*t(r)+.0722*t(n)>.179?"#000000":"#ffffff"}