Skip to content

API Reference

For: developers integrating the library. This is a reference, not a tutorial — if you're new, start with the Quick Start and Developer Guide.

This document describes the public API exposed by wafermap. For the system-level overview and recommended entry points, see Architecture.

How to read this document. §3 is the section map — find your entry point there. §4 (buildWaferMap, the data layer) plus §5 or §6 (the renderers) cover most applications; §7 adds the findings engine. Cross-references use §N.N notation throughout; shared types live in §12.


How much of this do I need?

Almost none of it. This is a reference, not a reading list — it documents every option so that the rare one you eventually need is written down, not so that you learn them.

A working map is two calls:

const result = buildWaferMap({ results, waferConfig, dieConfig });
renderWaferMap(document.getElementById('map'), result);

That renders with a full interactive toolbar — plot modes, colour schemes, zoom, export — with no options passed at all. Add analyzeWaferMap when you want the findings and summary panel, and renderWaferGallery in place of renderWaferMap for a lot. That is four functions, and it is the whole story for most integrations.

For scale: tsmap, a complete cross-platform desktop application built on this library, imports 14 of its ~100 exports. RenderOptions has 22 fields; a typical integration sets a handful. Everything else here is depth that stays out of your way until you go looking for it.

New to the library? Start with the Quick Start (a 5-minute tutorial), then the Guide for a feature walkthrough. Come back here when you need a specific option.

1 Coordinate system

x and y throughout this API are die grid positions (prober step coordinates) — integers such as −7, 0, 5. They are NOT millimetre values. They must be JavaScript number type — CSV parsers return strings; always cast with Number() or + before passing to buildWaferMap.

This matches what wafer test equipment outputs. The library converts grid positions to physical mm internally using the die size you provide.

prober outputs:  x=-5, y=3   (die grid position)
library computes: x_mm = -5 × 10 = -50 mm   (given die width = 10 mm)

Physical mm positions appear only on the Die output objects (die.physX, die.physY) and in the wafer model. You never need to compute or supply mm values.


2 Quick Start

The step-by-step tutorial is Quick Start; this is the condensed call shape as a memory jogger:

import { buildWaferMap } from '@wafertools/wafermap';
import { renderWaferMap } from '@wafertools/wafermap/render';

// x,y are prober step positions (die grid indices), not mm.
const result = buildWaferMap({
  results:   rows.map(r => ({ x: +r.x, y: +r.y, hbin: +r.hbin, testValues: { 1010: +r.testA } })),
  waferConfig: { diameter: 300, notch: { type: 'bottom' } },
  dieConfig:   { width: 10, height: 10 },
  testDefs: [{ testNumber: 1010, name: 'TestA', unit: 'V' }],
});

renderWaferMap(document.getElementById('map'), result);

The map renders with a full built-in toolbar — no extra HTML or JavaScript needed. To add a statistical findings panel, pass the result through analyzeWaferMap — see §7.1.


3 API overview

If you want the shortest path to the right entry point before diving into the type details, see Architecture. It explains which layer to use for data construction, rendering, analysis, and worker offloading.

graph TD
    bwm["buildWaferMap()<br/>data layer — no DOM"]
    rwm["renderWaferMap()"]
    rg["renderWaferGallery()"]
    awm["analyzeWaferMap()"]
    awl["analyzeWaferLot()"]
    wk["createWafermapWorker()"]

    bwm --> rwm
    bwm --> rg
    bwm --> awm
    bwm --> awl
    bwm --> wk

Everyday — the four functions almost every integration uses:

Section Description
4 buildWaferMap Data layer — primary entry point. Turns rows into a wafer map
5 renderWaferMap Interactive canvas map with toolbar. Works with no options
6 renderWaferGallery The same, for a whole lot: a grid of cards
7 Statistics / Findings analyzeWaferMap, analyzeWaferLot — findings and the summary panel

Occasional — reach for these when you hit the specific need:

Section Description
8 Web Worker Off-main-thread building, for very large lots
10 Package surface Which subpath exports what, and why the renderers aren't on the root
12 Important types Die, Wafer, TestDef and friends

Rarely needed — escape hatches. Skip unless something above can't do it:

Section Description
9 Low-level canvas API Deprecated: toCanvas — draw to a canvas you own, no toolbar
11 Advanced Pipeline Deprecated: buildView and the manual pipeline
13 Limitations Known constraints

4 buildWaferMap(input)

The primary entry point. Pass whatever data you have — prober step positions, optional geometry hints, or a pre-built die array. The function infers whatever is missing and returns a fully constructed wafer model.

Inference reads geometry from the extent of the data. When you supply only die positions, the wafer diameter and centre are derived from how far the data reaches. This is correct whenever the data reaches the true wafer edge — a fully-populated wafer, or a sparse one (skip-sampled or randomly sampled positions missing across the whole face). It is wrong for partial data — a contiguous region such as a half wafer, a single quadrant, or an off-centre cluster — because the extent stops short of the true edge, so the region is mistaken for a smaller full wafer and mis-centred. For partial data supply waferConfig.diameter and waferConfig.center — see §4.3 Inference levels. When the library detects likely-partial coverage with no anchor, it adds a structured warning to result.warnings (code 'partial-coverage').

Server-safe: buildWaferMap is a pure function with no DOM access or side effects. It can run in Node.js, Deno, a Web Worker, or any server-side environment.

import { buildWaferMap } from '@wafertools/wafermap';

4.1 Input

buildWaferMap accepts either an array of data points or an object. They are equivalent when no extra options are needed — the array form is just shorthand for { results: [...] }:

// Array form — shorthand, equivalent to passing { results }
buildWaferMap(results: DieResult[])

// Object form — use when you need geometry hints or other options.
// WaferMapInput is a union: pass either results OR lotStack, never both.
buildWaferMap(input: WaferMapInput)

WaferMapInput is a discriminated union:

// Shared base (WaferMapInputBase) — all fields are optional:
type WaferMapInputBase = {
  waferConfig?:      WaferConfig,      // physical wafer geometry (diameter, notch, orientation…)
  dieConfig?:        DieConfig,        // die size and coordinate conventions
  dies?:             Die[],            // pre-built die array; skips geometry generation
  reticleConfig?:    ReticleConfig,    // stepper field grid overlay
  passBins?:         number[],         // bins that pass (default [1]) — carried on the result as result.passBins
  ringCount?:        number,           // rings for ring overlays, ring yield and ring findings (default 4) — carried as result.ringCount
  retestPolicy?:     'last' | 'first' | 'best' | 'worst', // how to handle multiple results at the same (x,y); default 'last'
  edgeDieYieldMode?: 'exclude' | 'denominator-only', // default 'exclude'
  testDefs?:         TestDef[],        // named test definitions — one per testValues entry
  hbinDefs?:         BinDef[],         // named hard bin definitions — one per distinct hbin value
  sbinDefs?:         BinDef[],         // named soft bin definitions — one per distinct sbin value
  metadataFields?:   MetadataFieldDef[], // opts a die.metadata key into the 'metadata' plot mode — §4.1.10
}

// Single-wafer variant (WaferMapInputSingle):
type WaferMapInputSingle = WaferMapInputBase & {
  results?:  DieResult[]   // per-die measurements from the prober
  lotStack?: never          // passing both results and lotStack is a type error
}

// Lot-stack variant (WaferMapInputLotStack):
type WaferMapInputLotStack = WaferMapInputBase & {
  lotStack:  LotStackConfig  // collapse multiple wafers into one aggregated map
  results?:  never            // passing both results and lotStack is a type error and runtime error
}

type WaferMapInput = WaferMapInputSingle | WaferMapInputLotStack

All fields are optional. Supply what you know; the library handles the rest. Passing both results and lotStack on the same object is a type error and is rejected at runtime.

4.1.1 DieResult

A single die record from wafer test equipment.

{
  x:           number                      // die grid X position (prober step coordinate)
  y:           number                      // die grid Y position (prober step coordinate)
  testValues?: Record<number, number>      // preferred: test measurements keyed by stable test identity
                                           // e.g. { 1050: 1.42e-3, 1060: 0.487, 1070: 8.3e-12 }
                                           // the key is any stable integer per test — for example an STDF TEST_NUM,
                                           // a database test ID, or an application-defined constant
  testPass?:   Record<number, boolean>     // recorded per-test pass/fail verdicts keyed the same way (true = pass)
                                           // parametric tests: value in testValues, optionally the tester's verdict here
                                           // (e.g. STDF PTR TEST_FLG); functional tests (testType 'F'): verdict here ONLY —
                                           // they have no measured value. e.g. { 2001: true, 2002: false }
  hbin?:       number                      // hard bin assignment (physical sort result; STDF V4 range 0–32767)
  sbin?:       number                      // soft bin assignment (test-program failure category; independent 0–32767 space)
  siteNum?:    number                      // STDF site_num — which parallel test site tested this die
                                           // enables test-site analysis in analyzeWaferMap when ��2 distinct
                                           // values each appear on ≥3 dies (indicating a multi-site probe card)
  partId?:     number                      // STDF pir.part_id — tester-assigned identifier for this unit
                                           // at most fabs this encodes probe sequence (the step order across the wafer)
                                           // but the field is semantically neutral — its meaning is fab-specific
                                           // note: STDF part_id is 1-based; camelCase follows the library convention
}

A single test result: testValues: { 1050: 0.95 }

When a die position appears more than once in the results array (a retest), the retestPolicy field on WaferMapInput controls which result is kept. The die.retestCount field always records how many times that position appeared.

4.1.2 WaferConfig

{
  diameter?:      number         // wafer diameter in mm; inferred from grid extent × pitch if omitted
  center?:        { x: number, y: number }
                  // prober coordinate that lies at the physical wafer centre.
                  // Supply for partial data (a contiguous half/quadrant/slice that
                  // stops short of the wafer edge); anchors placement to the true
                  // centre. Not needed for sparse full-extent data. Does NOT change
                  // die.x/die.y labels.
                  // When omitted, the centre is inferred as the data midpoint (full-wafer assumption).
  notch?:         { type: 'top' | 'bottom' | 'left' | 'right' }
                  // physical orientation mark direction; standard dimensions derived from diameter:
                  //   ≤ 100 mm → 32.5 mm orientation flat  (SEMI M1)
                  //   ≤ 150 mm → 57.5 mm orientation flat  (SEMI M1)
                  //   > 150 mm → V-notch ~3.5 mm wide, 1.25 mm deep  (SEMI M1)
  orientation?:   number         // degrees CW to rotate the die grid on screen; default 0 (see note below)
  edgeExclusion?: number         // exclusion band width in mm measured inward from the wafer edge; dies in this band are dimmed
                                 // how these dies affect yield is controlled by the top-level edgeDieYieldMode option (§4.1.10)
  metadata?:      WaferMetadata  // arbitrary lot/wafer-level data attached to the view (lot ID, date, etc.)
}

orientation note: positive values rotate the die grid clockwise. The notch/flat position is controlled by notch.type and is not affected by orientation — it stays fixed as the physical alignment mark.

4.1.3 DieConfig

{
  width?:              number   // die width in mm (= X step pitch); enables physical mm coordinates
  height?:             number   // die height in mm (= Y step pitch); enables physical mm coordinates
  coordinateOrigin?:   {
    // where the prober places coordinate (0,0) on the wafer grid
    type: 'center'           // default — grid already centred; centroid offset applied automatically
        | 'LL'               // (0,0) at lower-left corner (standard STDF/KLA output)
        | 'UL'               // (0,0) at upper-left corner — positive Y runs downward (flips display Y)
        | 'LR'               // (0,0) at lower-right corner — positive X runs leftward (flips display X)
        | 'UR'               // (0,0) at upper-right corner — both axes flipped
        | 'custom'           // manual offset: centre = (0,0) + offset in grid steps
    offset?: { x: number; y: number }   // grid-step offset to the true centre; only used when type is 'custom'
  }
  yAxisDirection?: 'up' | 'down'     // which direction Y increases on the prober; 'down' for row/matrix probers (default 'up')
  xAxisDirection?: 'right' | 'left'  // which direction X increases; 'left' for backside or mirrored probing (default 'right')
}

When width and height are omitted, the library estimates die dimensions from the grid layout using nearest-neighbour step analysis first, falling back to the circular-wafer aspect-ratio constraint.

4.1.4 ReticleConfig

{
  width:      number               // stepper field width in number of dies (e.g. 4 means 4 dies wide)
  height:     number               // stepper field height in number of dies
  anchorDie?: { x: number; y: number }
               // die grid index (x, y), in original die coordinates (die.x/die.y),
               // that sits at the reticle field's min-x/min-y corner (bottom-left,
               // since +Y is up) — i.e. it becomes the leftmost, bottom-most die
               // of the field it belongs to. Shifts the entire reticle grid so
               // this die aligns to a field boundary.
               // Default {0,0} — die (0,0) is at a corner.
}

When provided, reticle overlays are shown by default (showReticle defaults to true).

4.1.5 LotStackConfig

Collapse data from multiple wafers into a single map before rendering. When lotStack is present the top-level results field is ignored.

{
  results:    DieResult[][]  // input data — one DieResult[] per wafer in the lot
  method:     // aggregation applied per die position across all wafers:
    | 'mean'       // arithmetic mean of values → testValues[0]
    | 'median'     // median of values → testValues[0]
    | 'stddev'     // sample standard deviation of values → testValues[0]
    | 'min'        // minimum value across lot → testValues[0]
    | 'max'        // maximum value across lot → testValues[0]
    | 'count'      // number of wafers that provided a value at this position → testValues[0]
    | 'countBin'   // how many wafers had targetBin at this position → testValues[0]
    | 'mode'       // most frequent bin across wafers → hbin
    | 'percent'    // percentage of wafers that had targetBin → testValues[0] in [0,100]
  targetBin?: number   // bin value to count or measure; required for 'countBin' and 'percent'
}

4.1.6 passBins

passBins?: number[]   // default [1]  (industry convention: bin 1 = pass)

Bin values that count as pass for yield calculation. Set to [] to suppress yield.

4.1.7 retestPolicy

retestPolicy?: 'last' | 'first' | 'best' | 'worst'   // default 'last'

Controls how the library handles multiple results for the same die position (retests). In wafer test it is common for a die to be tested more than once — for example after a recontact, a temperature retest, or a continuity retest.

Policy Behaviour
'last' (default) Keep the most recent result — the last entry in results for that position
'first' Keep the earliest result — the first entry in results for that position
'best' Keep the best result using passBins as the primary criterion: a pass result always beats a fail result. When both candidates are in the same pass/fail category, the lower hbin number is the tiebreaker. Falls back to 'last' when any candidate has no hbin.
'worst' Keep the worst result: a fail result always beats a pass result. When both are in the same category, the higher hbin number wins. Falls back to 'last' when any candidate has no hbin.

Regardless of which policy is active, die.retestCount is always set on any die that appeared more than once in the input. Use it to identify retested dies in your own analysis without needing to re-scan the raw results.

// Last result wins (default — no field needed):
buildWaferMap({ results })

// Explicitly keep first result:
buildWaferMap({ results, retestPolicy: 'first' })

// Check how many retests occurred after the map is built:
result.dies.filter(d => d.retestCount !== undefined)
  .forEach(d => console.log(`Die (${d.x},${d.y}) tested ${d.retestCount} times`));

4.1.8 TestDef

Named definition for one test parameter. The toolbar mode dropdown always offers one entry per test — using testNumber as the label when testDefs is absent. When testDefs is provided, tooltips show "Idsat: 1.23 mA" with the test name and SI-scaled unit; without it they fall back to "Test 1050: 1.23 mA".

{
  testNumber:  number  // required: stable test identity matching the key used in DieResult.testValues
                       // e.g. an STDF TEST_NUM, a database test ID, or an application-defined constant
  name:        string  // e.g. "Idsat", "Vth", "Continuity"
  unit?:       string  // SI base unit, e.g. "A", "V", "Ω", "F" — the formatter applies SI prefixes
                       // automatically (0.03 Ω → "30 mΩ"), so always pass the base unit, never a
                       // pre-scaled unit like "mA" or "µV"
  logScale?:   boolean // when true, value normalization and the colorbar use log₁₀ scale for this test
                       // silently falls back to linear when any die value is ≤ 0; default false
  limitLow?:   number  // lower specification limit in the same units as the test value
                       // values below this are out-of-spec; drives out-of-spec coloring and spec yield stats
  limitHigh?:  number  // upper specification limit in the same units as the test value
                       // values above this are out-of-spec
                       // both limits are optional independently — one-sided limits are valid
  testType?:   'P' | 'F'  // 'P' = parametric (continuous measured value, the default),
                       // 'F' = functional (pass/fail outcome ONLY, no measured value —
                       // e.g. an STDF FTR; the verdict lives in DieResult.testPass).
                       // Functional tests stay selectable in value mode but always render
                       // as test pass/fail (solid green/red with a Pass/Fail legend), and
                       // are excluded from all parametric statistics — per-test stats
                       // tables, capability, correlation, distribution charts, value
                       // stacks, and regional value findings (a mean or Cpk of a binary
                       // outcome would be meaningless). They get pass-rate analysis
                       // instead: stats.functionalYield, "Functional Tests" tables, and
                       // regional pass-rate findings (kind 'functionalTest')
}

testNumber must match the key used in DieResult.testValues / DieResult.testPass.

isParametricTest(def) (exported; deprecated — removed in 0.31.0) returns false only for testType: 'F' — an undefined def or undefined testType counts as parametric, so untyped callers are unaffected.

Legacy functional encoding: callers that predate DieResult.testPass encoded a functional outcome as a testValues entry of 1 (pass) / 0 (fail). That data keeps working everywhere — rendering, stats, and findings all read verdicts through getTestPassStatus (§10.1), which documents the fallback. New code should write testPass and leave functional tests out of testValues entirely.

4.1.9 BinDef

Named definition for one bin number. Used for both hard bin (hbinDefs) and soft bin (sbinDefs) — the shape is identical but the number spaces are independent.

Per STDF V4, hard bins and soft bins each range 0–32767. Bin 1 in hard bin space and bin 1 in soft bin space are different things and may have different names — always pass them as separate arrays.

{
  bin:    number   // the numeric bin value this defines
  name:   string   // e.g. "Pass", "Contact Open", "Vth - Hi NMOS"
  color?: string   // optional CSS colour, e.g. "#2ecc71" — wins over the bin colour scheme for this bin (§11.19)
}

Hard bins (hbinDefs) are the physical sort result — where the part goes on the handler. Soft bins (sbinDefs) are the logical test-program classification — the failure category as determined by the test algorithm, used for debug and yield analysis. Many soft bins typically map to one hard bin.

4.1.10 edgeDieYieldMode

edgeDieYieldMode?: 'exclude' | 'denominator-only'   // default 'exclude'

Controls how dies within the edge exclusion zone (waferConfig.edgeExclusion) are treated in yield calculation.

Value Behaviour
'exclude' (default) Edge dies are excluded from both numerator and denominator. YieldSummary.yieldPercent reflects only the interior dies.
'denominator-only' Edge dies are counted in the denominator but never in the pass numerator. Produces gross die yield — the industry metric for quantifying yield loss due to edge effects. YieldSummary.yieldPercentGross is populated with this value; yieldPercent is also populated for comparison.
const result = buildWaferMap({
  results,
  waferConfig:      { diameter: 300, edgeExclusion: 3 },
  dieConfig:        { width: 8, height: 12 },
  edgeDieYieldMode: 'denominator-only',
});

const { yieldPercent, yieldPercentGross } = result.yield;
// yieldPercent      — interior-only yield (edge dies excluded entirely)
// yieldPercentGross — gross die yield (edge dies counted against you)

4.1.11 MetadataFieldDef

Named definition for one die.metadata key, opting it into the 'metadata' plot mode — a generic categorical/layout view, distinct from test results and bins. Use this for any per-die classification that isn't a test outcome: which project a die belongs to on a multiproject wafer, vendor/third-party ownership, reserved/shared/unassigned areas, or any other host-defined grouping already carried in die.metadata.

{
  key:     string    // the die.metadata key this definition applies to
  label?:  string     // display name for the toolbar entry and map title — default: Title Case of key
  values?: Array<{ value: string; label?: string; color?: string }>  // optional per-value overrides
}

A key is only offered in the toolbar's "Metadata" mode-menu section when it appears in metadataFields and at least one die actually has that key set — presence in metadataFields is an explicit opt-in, never auto-detected. values is optional per field: distinct values with no override are still shown, auto-labeled with the raw (stringified) value and auto-colored from an ordered qualitative palette (assigned in natural alphanumeric order by value — so D2 precedes D10 rather than following it — making colors stable across reloads and the legend order predictable — not the pass/fail-flavored palette hardBinColor uses, since an arbitrary metadata field has no universal "good/bad" meaning).

const result = buildWaferMap({
  results: [
    { x: 4, y: -2, hbin: 1, metadata: { project: 'Project A', device: 'Device 12' } },
    { x: 5, y: -2, metadata: { project: 'vendor' } }, // vendor die — no test/bin data at all
  ],
  waferConfig: { diameter: 300 },
  dieConfig:   { width: 10, height: 10 },
  metadataFields: [
    { key: 'project', label: 'Project', values: [
      { value: 'Project A', color: '#4e79a7' },
      { value: 'vendor',    label: 'Third-party vendor', color: '#bab0ac' },
    ] },
  ],
});

renderWaferMap(container, result, { viewOptions: { plotMode: 'metadata', activeMetadataKey: 'project' } });

Key properties of this mode:

  • Coexists with test/bin data — a die can carry hbin/testValues and a metadata classification at the same time; 'metadata' is just another selectable toolbar view of the same underlying die, the same way hardBin/softBin/value already are. die.metadata already renders in every tooltip regardless of plot mode, so switching into 'metadata' mode changes the map's colour/legend without changing what the tooltip shows.
  • No lot-stacking. 'metadata' is deliberately excluded from the stacked/lot-aggregation modes (stackedValues/stackedBins/stackedSoftBins) — a die's layout classification is a constant of the design, not a per-wafer measurement, so there is nothing meaningful to aggregate across a lot.
  • Never affects yield. die.metadata was never part of the yield-eligibility pipeline, so a 'metadata'-classified die's yield/pass-fail status (if it has one) is entirely unaffected by this mode.
  • Click-to-highlight in the legend, exactly like hardBin/softBin: clicking a legend swatch dims every die except that value (highlightMetadataValue, the string-keyed analogue of highlightBin); clicking the same swatch again clears it.
  • Reuses wafer geometry, tooltip, selection, zoom, and PNG export unchanged — none of those are plot-mode-aware. The one thing genuinely new is the colour fill, the legend, and the toolbar entry.

4.1.12 standardDiameters

standardDiameters?: number[]   // default [100, 125, 150, 200, 300]

The wafer diameters (mm) treated as standard when sanity-checking an inferred diameter. A diameter you supply is never second-guessed; this is consulted only when the library had to size the wafer from the die extent, and it is what decides whether the non-standard-diameter advisory (§4.2.2) fires.

It replaces the default rather than adding to it, so spread it to extend:

import { buildWaferMap } from '@wafertools/wafermap';

// a line that also runs 3-inch
buildWaferMap({ results, standardDiameters: [100, 125, 150, 200, 300, 76.2] });

// genuinely non-standard substrate — panels, reclaim, odd R&D shapes
buildWaferMap({ results, standardDiameters: [] });

[] disables the check entirely, and is the intended opt-out for a substrate that is not on the ladder — better than suppressing every geometry advisory to silence one that does not apply to your line.

STANDARD_WAFER_DIAMETERS_MM (deprecated — removed in 0.31.0) is exported from the root entry point as a readonly number[], so host code can read, extend or compare against the SEMI M1 ladder without restating it.

4.2 Return value

{
  wafer:         Wafer          // resolved wafer model (diameter, radius, center, notch, orientation)
  dies:          Die[]          // all dies inside the wafer boundary, with testValues/hbin/sbin attached
  plotMode:      PlotMode       // the plot mode chosen by buildWaferMap ('hardBin', 'value', etc.)
  metadata:      WaferMetadata | null  // wafer metadata from waferConfig.metadata
  isLotStack:    boolean        // true when built from lotStack input
  aggrMethod?:   string         // lot-stack aggregation method ('mean', 'median', 'countBin', …); undefined for single wafers
  lotSize?:      number         // number of wafers aggregated when built from lotStack; undefined for single wafers
  hbinDefs?:     BinDef[]       // named hard bin definitions passed to buildWaferMap
  sbinDefs?:     BinDef[]       // named soft bin definitions passed to buildWaferMap
  testDefs?:     TestDef[]      // named test definitions passed to buildWaferMap
  metadataFields?: MetadataFieldDef[]  // named metadata-field definitions passed to buildWaferMap — §4.1.11
  reticles:      Reticle[]      // generated reticle geometry — wired automatically when passed as a WaferMapDisplayItem
  reticleConfig: ReticleConfig | undefined  // the reticle config that was used; passed through to analyzeWaferMap automatically
  units:   'mm' | 'normalized'   // coordinate space of die.physX/die.physY and wafer dimensions
  warnings: WaferWarning[]       // structured geometry-inference advisories — always present (may be empty).
                                  // Surfaced automatically by renderWaferMap/renderWaferGallery in the
                                  // toolbar's warning indicator; read here for programmatic use. §4.2.2
                                  // { code: string; message: string;
                                  //   severity?: 'error' | 'warning' | 'info'; confidence?: number }
                                  // The first three geometry codes below are severity 'error': they mean
                                  // die positions may be wrong, not that a feature is missing.
                                  // Codes:
                                  //   'partial-coverage'  — data does not span a full wafer; inferred
                                  //     diameter/centre may be wrong. Supply waferConfig.center + .diameter.
                                  //   'geometry-conflict' — waferConfig.diameter AND dieConfig.width/
                                  //     height were BOTH supplied, and are too small to contain the
                                  //     probed dies. A die with results is a real prober position and is
                                  //     always fully on the wafer, so the two supplied values contradict
                                  //     each other; check them against the real device.
                                  //   'non-standard-diameter' — pitch supplied without a diameter, and the
                                  //     wafer sized from the die extent landed off the standard ladder
                                  //     (100/150/200/300 mm) — evidence the probed grid did not reach the
                                  //     edge. Supply waferConfig.diameter. NOTE: an inferred PITCH raises
                                  //     nothing: it is derived to fit the diameter, so it is always
                                  //     self-consistent. And 'geometry-conflict' is never raised for an
                                  //     inferred pitch — pitch is a free scaling parameter, so with
                                  //     no supplied pitch there is always one that "fits", and a fit
                                  //     check would otherwise fire on perfectly good full-wafer data.
                                  //   'edge-exclusion-exceeds-radius' — severity 'warning', not 'error':
                                  //     waferConfig.edgeExclusion is larger than the resolved wafer radius
                                  //     (most likely when the diameter was itself inferred from sparse
                                  //     data). The excluded band is clamped to the whole wafer rather than
                                  //     silently producing a smaller, wrong ring.
  inference: {
    wafer:    { confidence: number; method: string }   // how diameter was resolved; confidence 0–1.
                                                        // method is 'inferred-partial' when partial data was detected
    diePitch: { confidence: number; units: 'mm' | 'normalized' }  // how die size was resolved
    grid:     { confidence: number }                   // quality of the grid index assignment
  }
  dataCoverage: {
    filledDies:       number   // dies with at least one value or bin attached
    totalDies:        number   // POSITIONED dies inside the wafer boundary (including partial) — 0 for a
                                // fully coordinate-less wafer; see unpositionedDies below, not this, for
                                // "how many dies does this wafer actually have"
    edgeExcludedDies: number   // dies whose centres fall within the edge exclusion band
    ratio:            number   // filledDies / totalDies ∈ [0, 1] — undefined/misleading when totalDies is 0
    unpositionedDies: number   // dies with no reported x/y at all (see §12.1's coordinate-less note) —
                                // always present, 0 when every die has a position
  }
  passBins: number[]    // the pass bins given to buildWaferMap. renderWaferMap, renderWaferGallery (per wafer)
                         // and analyzeWaferMap read these — never repeat them in a render or analysis call
  ringCount: number     // the ring count given to buildWaferMap (default 4) — read by the renderers, analysis and reports
  yield: YieldSummary   // pass/fail statistics computed against passBins — NOT scoped to positioned dies,
                         // unlike dataCoverage above; a coordinate-less die with bin data still counts
}

4.2.1 YieldSummary

{
  passDies:          number          // dies with a bin in passBins
  failDies:          number          // full dies inside wafer with a bin not in passBins
  edgeExcludedDies:  number          // dies within the edge exclusion zone
  partialDies:       number          // dies straddling the wafer boundary — always 0 for a
                                     // `results`-based map (§12.1)
  totalDies:         number          // passDies + failDies (edge-excluded not included)
  yieldPercent:      number | null   // (passDies / totalDies) × 100 ∈ [0, 100]; null when no bin data
  yieldPercentGross: number | null   // (passDies / (passDies + failDies + edgeExcludedDies)) × 100 ∈ [0, 100];
                                     // only set when edgeDieYieldMode: 'denominator-only'; otherwise null
}

Partial dies are excluded from both numerator and denominator. Edge-excluded dies are excluded by default (edgeDieYieldMode: 'exclude'); set edgeDieYieldMode: 'denominator-only' to include them in the denominator for gross die yield.

result.yield.yieldPercent vs summary.stats.yieldPercent — both are in [0, 100] and can differ when you pass custom options to analyzeWaferMap (e.g. a different edgeDieYieldMode or passBins). Use result.yield for rendering and quick checks; use summary.stats.yieldPercent when you need the yield that is consistent with the findings analysis.

units tells you the coordinate space of the physical coordinates (die.physX, die.physY) and wafer dimensions; die.x/die.y remain die grid positions (prober step coordinates):

  • 'mm' — at least one physical dimension was known (die size or wafer diameter); physical coordinates (die.physX/die.physY and wafer dimensions) are expressed in millimetres.
  • 'normalized' — only grid positions were supplied; physical coordinates are in normalized units (aspect ratio preserved) with pitchX = 1 normalized unit by convention.

4.3 Inference levels

The library adapts to whatever geometry context you provide. Four distinct levels:

Provided Inferred units
grid positions only Pitch from nearest-neighbour step analysis; diameter from grid extent 'normalized'
grid positions + die size Diameter from grid extent × pitch 'mm'
grid positions + wafer diameter Die size from diameter / grid_extent 'mm'
grid positions + die size + diameter Nothing — fully specified 'mm'

All four levels assume the data spans a full, roughly symmetric wafer centred near the prober origin. The diameter and centre are derived from the extent of the data you pass. See "Minimum geometry for partial data" below before relying on inference for anything less than a full wafer.

Diameter snapping: inferred diameters snap to industry-standard sizes. 100 mm, 150 mm, 200 mm, and 300 mm are preferred (±10% tolerance); other SEMI standard sizes (25 / 50 / 75 / 450 mm) are tried next (±20%); remaining values are rounded to the nearest 10 mm.

Origin: defaults to 'center' (centroid offset applied automatically). Set coordinateOrigin: { type: 'LL' } explicitly for standard STDF/KLA output where (0,0) is at the lower-left corner.

4.2.2 WaferWarning

The library's one warning vocabulary. Raised by geometry inference on WaferMapResult.warnings, and by analysis on StatsSummary.stats.warnings.

{
  code:      string   // stable machine-readable key — BRANCH ON THIS, not on message
  message:   string   // human-readable, suitable for direct display
  severity?: 'error' | 'warning' | 'info'   // default 'warning' when absent
  confidence?: number // inference confidence 0–1, when one applies
}

Codes

Code Severity Meaning
partial-coverage error Data does not span a full wafer; inferred diameter/centre may be wrong and dies may be mis-positioned. Supply waferConfig.center + .diameter.
geometry-conflict error waferConfig.diameter and dieConfig.width/height were both supplied and cannot contain the probed dies.
non-standard-diameter warning A die pitch was supplied without a diameter, so the wafer was sized from the die extent — and the result is off the standard wafer-size ladder (SEMI M1: 100/150/200/300 mm and the smaller legacy sizes). Silicon only comes in those sizes, so e.g. 210 mm is evidence the probed grid did not reach the wafer edge and the wafer is really larger. Dies are then placed against a wafer that is too small, which moves them between rings and changes ring/edge findings. Supply waferConfig.diameter — or, if your line genuinely runs a size that is not on the ladder, extend or empty the ladder itself with standardDiameters (§4.1.12) rather than muting every geometry advisory. There is no matching advisory for an inferred pitch: that is derived to fit the supplied diameter, so it is self-consistent by construction and there is nothing to check it against.
diameter-exceeds-die-extent warning A supplied waferConfig.diameter that the probed dies fill less than 75% of the radius. The mirror of geometry-conflict, which asks whether the dies fit; this asks whether they fill. An over-large wafer is not harmless — ring bands are equal-radius, so it crushes dies into the inner rings and empties the outer ones (at a 10× diameter every die lands in ring 1), and ring/quadrant/edge findings then describe the assumed wafer rather than the probed area. A genuinely partial map looks identical, so the message names both causes. Not raised when waferConfig.center is supplied (that is the documented way to position partial data deliberately) or below 20 dies (too few for the extent to be evidence, and too few for ring analysis to report anything).
test-count-capped warning More tests found than analyzeWaferMap will analyse, so no test findings were computed at all. Pass testNumbers to scope it.
edge-exclusion-exceeds-radius warning waferConfig.edgeExclusion exceeds the resolved wafer radius (most likely with an under-inferred diameter). The excluded band is clamped to the whole wafer instead of silently producing a smaller, wrong ring.
bin-colors-shared warning Raised by the renderers (not buildWaferMap) for the bin map on screen: some bins are drawn in a colour another bin also has — more bins than the bin colour scheme has distinct colours, or a BinDef.color repeats one. Every die is drawn correctly; colour alone cannot separate those bins. A gallery states it once for all its wafers.
pass-bins-mixed warning Raised by renderWaferGallery: its wafers were built with different pass bins, and some hard bins pass on one wafer and fail on another. Every wafer's own verdicts and yield are correct; a bin has one colour and one legend row, so the named bins are shown as failing there.
ring-count-mixed warning Raised by renderWaferGallery: its wafers were built with different ringCounts. Each card and each wafer's findings use their own; the lot-level ring figures (Summary panel, report, Insights) use the count the message names.
input-field-removed error Raised by buildWaferMap: the input used a name removed in an earlier release — data, die, stack, values, TestDef.index, dieConfig.origin, waferConfig.flat, reticleConfig.anchor or lotStack.aggr. It is not honoured, so what it described is missing from the map (for data and values, the data itself). The message names each one and its replacement; it is also logged to the console, for a caller that does not read result.warnings.

severity is about trust in what is on screen, not about how loud the message is: 'error' means the map may be positionally wrong; 'warning' means something expected is missing or degraded but what is drawn is correct. The union of code is deliberately open to string so new advisories are not a breaking change — switch with a default branch.

These are surfaced automatically — see WarningsOptions.

Minimum geometry for partial data

Inference works backwards from the data's bounding extent. What matters is whether the data reaches the true wafer edge:

  • Sparse data — positions missing across the whole wafer (systematic skip-sampling, e.g. 1-in-4, or random sampling). The extent still reaches the edge, so diameter and centre infer correctly. No geometry hints required.
  • Partial data — a contiguous region that stops short of the edge: a half wafer, a single quadrant, a slice, or an off-centre cluster. The extent understates the wafer, so the region is mistaken for a smaller full wafer and re-centred on its own midpoint. Inference is wrong here.

An edge ring / annulus is a middle case: only outer dies are present, but they reach the true edge, so the diameter is right — only the empty interior is "missing", which is harmless.

For partial data, supply both:

Field Meaning
waferConfig.diameter the true wafer diameter in mm
waferConfig.center the prober coordinate (x, y) that lies at the physical wafer centre

waferConfig.center anchors placement to the real centre. It does not change the public die.x / die.y labels — those remain the original prober coordinates. (Supplying dieConfig.width/height for the pitch is recommended too, so coordinates are in real mm.)

Detection is heuristic: the library flags likely-partial coverage by how far the data centroid sits from its bounding-box centre. Contiguous partial regions are caught; an off-centre cluster small enough to look like a tiny full wafer, and an edge ring (centroid-symmetric), are not flagged — when in doubt, set waferConfig.center explicitly rather than relying on the warning.

// Right half of a 300 mm wafer; prober (0,0) is the wafer centre.
const result = buildWaferMap({
  results,                                   // prober x ∈ [0..15], y ∈ [-15..15]
  waferConfig: { diameter: 300, center: { x: 0, y: 0 } },
  dieConfig:   { width: 10, height: 10 },
});

When the library detects likely-partial data and no center was supplied, it adds a structured warning to result.warnings (code 'partial-coverage') and sets result.inference.wafer.method to 'inferred-partial' — check these programmatically rather than relying on console output.

4.4 Examples

Minimal — grid positions only (normalized units):

const result = buildWaferMap([
  { x: 0, y:  0, testValues: { 1050: 0.95 } },
  { x: 1, y:  0, testValues: { 1050: 0.87 } },
  { x: 0, y: -1, testValues: { 1050: 0.91 } },
]);
// result.units === 'normalized'

With die size — physical mm coordinates:

const result = buildWaferMap({
  results:   data,
  dieConfig: { width: 10, height: 10 },
});
// result.units === 'mm'

Fully specified with notch:

const result = buildWaferMap({
  results:     data,
  waferConfig: { diameter: 300, notch: { type: 'bottom' }, orientation: 90 },
  dieConfig:   { width: 10, height: 10 },
});

With bin data and edge exclusion:

const result = buildWaferMap({
  results:     csvRows.map(r => ({ x: Number(r.x), y: Number(r.y), hbin: Number(r.hbin) })),
  waferConfig: { diameter: 200, edgeExclusion: 3 },
  dieConfig:   { width: 8, height: 8 },
});
console.log(result.yield.yieldPercent);

Multiple tests and bins in a single pass:

const result = buildWaferMap({
  results: rows.map(r => ({
    x: +r.x, y: +r.y,
    testValues: { 1010: +r.testA, 1020: +r.testB, 1030: +r.testC },
    hbin: +r.hbin,
    sbin: +r.sbin,
  })),
  testDefs: [
    { testNumber: 1010, name: 'Idsat', unit: 'A' },
    { testNumber: 1020, name: 'Vth',   unit: 'V' },
    { testNumber: 1030, name: 'Ioff',  unit: 'A' },
  ],
  dieConfig: { width: 10, height: 10 },
});

Reticle overlay phased to die (2, 1):

const result = buildWaferMap({
  results:   data,
  dieConfig: { width: 10, height: 10 },
  reticleConfig: { width: 4, height: 2, anchorDie: { x: 2, y: 1 } },
});

Multi-wafer lot stack — count bin 2 failures across six wafers:

const result = buildWaferMap({
  waferConfig: { diameter: 300 },
  dieConfig:   { width: 10, height: 10 },
  lotStack: {
    results:   [wafer1, wafer2, wafer3, wafer4, wafer5, wafer6],
    method:    'countBin',
    targetBin: 2,
  },
});

Row-based prober (y increases downward, origin at upper-left):

const result = buildWaferMap({
  results:   data,
  dieConfig: { width: 10, height: 10, coordinateOrigin: { type: 'UL' } },
});

Retests — keep first result, surface retest count in tooltip:

// Raw results may include the same (x, y) more than once.
// 'first' keeps the initial test; 'last' (default) keeps the most recent.
const result = buildWaferMap({
  results:      rawResults,
  retestPolicy: 'first',
  waferConfig:  { diameter: 300, notch: { type: 'bottom' } },
  dieConfig:    { width: 10, height: 10 },
});

// die.retestCount is set (to the total count) whenever a position was retested.
const retested = result.dies.filter(d => d.retestCount !== undefined);
console.log(`${retested.length} die positions were retested`);
// e.g. → "47 die positions were retested"
// The built-in tooltip automatically shows "Retests: N" for retested dies.

4.5 Post-enrichment

When you need to attach additional values after the map is built, use getDieKey for stable lookups:

import { buildWaferMap, getDieKey } from '@wafertools/wafermap';

const result = buildWaferMap({ results: primaryData, waferConfig, dieConfig });

const rowMap = new Map(rows.map(r => [getDieKey({ x: +r.x, y: +r.y }), r]));
const enrichedDies = result.dies.map(d => {
  const row = rowMap.get(getDieKey(d));
  if (!row) return d;
  return {
    ...d,
    testValues: { 1010: +row.testA, 1020: +row.testB, 1030: +row.testC },
    hbin:       +row.hbin,
    sbin:       +row.sbin,
  };
});

getDieKey always use this for stable die lookups rather than ad-hoc template literals — it guarantees a consistent "x,y" format across grid offset corrections.


5 renderWaferMap(container, result, options?)

A fully self-contained interactive wafermap. Accepts a WaferMapResult directly, owns view building internally, and provides a built-in toolbar that appears on hover — wafermap-specific controls always in the same place.

renderWaferMap(container: HTMLElement, result: RenderableWaferMap, options?: RenderOptions): WaferMapController

options is optional, and mostly stays that way. RenderOptions has 22 top-level fields and this section documents all of them, but the call above works with none: you get the toolbar, plot modes, colour schemes, zoom and pan, tooltips, die selection and PNG export by default.

For calibration, tsmap — a full desktop application on this library — passes 7: viewOptions (initial plot mode and colour scheme), summaryPanel, insights, downloadFilename, userGuideExtension and showHelpButton. Those six, plus onSaveImage/onSaveText if you want exports routed through your own save dialog, cover the overwhelming majority of integrations.

Everything else in this section exists for a specific need — a host that draws its own chrome, an embedded map that must follow a host theme, a wafer with no position data. Search for the problem you have; don't read forward.

RenderableWaferMap is { wafer, dies } plus every other WaferMapResult field as optional. A WaferMapResult satisfies it, so the usual buildWaferMaprenderWaferMap path is unchanged; the wider type exists because renderWaferGallery renders each card through this same function, and a card is a WaferMapDisplayItem that carries no dataCoverage, viewport, legendBox, binLegendRows or reticleConfig. Supply what you have — anything absent is derived or skipped rather than assumed.

renderWaferMap accepts any block HTMLElement as container — the function creates and manages its own <canvas> inside it, sized to fill the container (width: 100%; height: 100%). Width comes from document flow, but the canvas can only fill a resolved height. A plain block <div> in normal flow works (it grows to the map), but a flex or grid child whose ancestors never resolve a height collapses to zero — the map is then invisible and the library logs a warning. Give the container a height with any of:

<!-- Fixed size: -->
<div id="map" style="width: 600px; height: 600px;"></div>

<!-- Responsive square (height follows width): -->
<div id="map" style="width: 100%; aspect-ratio: 1;"></div>
// Or let the library size it — no container CSS needed:
renderWaferMap(container, result, { height: 600 }); // px, or '70vh', etc.

See Troubleshooting → Map is blank, invisible, or the wrong height for all four valid sizing patterns. The toolbar gives users direct access to every display option without any app-level chrome: plot mode, colour scheme, ring and quadrant overlays, die labels, rotate, flip, zoom, box-select, and PNG download. An expand button (⛶) in the toolbar opens the map in an enlarged modal overlay using canvas reparenting — no second controller is created. A maximise button inside the modal grows it to fill the window.

import { buildWaferMap } from '@wafertools/wafermap';
import { renderWaferMap } from '@wafertools/wafermap/render';

const result = buildWaferMap({ results, passBins });
const ctrl = renderWaferMap(document.getElementById('map'), result, { showToolbar: true });

5.1 WaferViewOptions

viewOptions controls the initial display state of the map — which plot mode to show, the colour scheme, overlays, orientation, and so on. Every field is optional; the toolbar lets users change all of them at runtime. Pass viewOptions inside RenderOptions:

renderWaferMap(container, result, {
  viewOptions: {
    plotMode:      'value',
    activeTest:    1060,       // testNumber to show (must match a testDef.testNumber)
    valueColorScheme: 'mako',
    showDieLabels: true,
  },
});

To read or update options programmatically after mount, use the controller:

const ctrl = renderWaferMap(container, result, options);

ctrl.getOptions();                         // → current WaferViewOptions snapshot
ctrl.setOptions({ plotMode: 'softBin' });  // merge — only listed keys change

Field reference

Field Type Default Notes
plotMode PlotMode 'hardBin' 'hardBin' | 'softBin' | 'value' | 'stackedValues' | 'stackedBins' | 'stackedSoftBins' | 'metadata'
binColorScheme string 'default' Bin palette for hardBin/softBin. Built-in: 'default', 'accessible' (colour-blind safe). Custom palettes via registerBinColorScheme() (§11.19). Pass bins take the palette's pass (green) colours and fail bins its fail colours, each chosen by bin number so a bin is the same colour in every lot — see resolveBinColors.
valueColorScheme string 'default' Value gradient for value and the stacked modes (a stacked-bin map is a value map: each position's occurrence rate). Built-in: 'default' (Viridis), 'cividis' (colour-blind safe), 'greyscale', 'plasma', 'inferno', 'mako', 'traffic', 'jet'. Every one but 'traffic' and 'jet' reads low = dark, high = light. Custom gradients via registerValueColorScheme(). Separate from binColorScheme, so switching plot mode never resets either. Neither applies in 'metadata' mode (always the dedicated ordered palette + MetadataFieldDef.values[].color overrides — §4.1.11).
reverseValueScheme boolean false Flip the value gradient so high values take its low-end colour. The Colour scheme menu offers it as Reverse gradient. Use it for a parameter whose low end is the notable one, or for monochrome print where more ink should mean more. Applies to the dies, the colorbar and the mapless summary together — resolve any gradient of your own through resolveValueColorFn(name, reversed) so it cannot disagree with them.
useDefinedBinColors boolean true Honour BinDef.color where a bin definition supplies one. The Palette menu offers it as Use colours from bin definitions, only when some definition carries a colour.
activeTest number 0 testNumber to display in value mode — must match a testDef.testNumber, not a positional index
activeMetadataKey string die.metadata key to display in 'metadata' mode — must match a metadataFields[].key (§4.1.11)
passFailDisplay 'off' \| 'spec' \| 'test' 'off' Requested pass/fail display for value mode. 'spec' colours dies by spec-limit judgement (green / blue fail-low / red fail-high; degrades to 'off' when the active test has no limits). 'test' colours dies by the tester's own verdict from die.testPass (green pass / red fail, undirected; degrades to 'off' when no die has a verdict for the active test). The library resolves the effective display — a functional active test (testType: 'F') always renders as 'test' regardless of this option. Both solid displays replace the colorbar with a Pass/Fail legend carrying per-category die counts, and the map title's secondary line names which is shown (Spec pass/fail vs Tester pass/fail vs Functional pass/fail). Toggled via the Overlays toolbar menu, whose two entries appear only when valid for the active test.
highlightBin number Dim all bins except this one. Clicking a bin/soft-bin legend swatch toggles it.
highlightMetadataValue string 'metadata' mode's analogue of highlightBin — dim every die except this metadata value. Clicking a metadata legend swatch toggles it.
valueRange [number, number] \| { test, range } auto Explicit range for value colour normalization; overrides colorbarRangeMode. Tuple applies to the active test (caller owns the coupling). Object { test, range } applies only when test matches the active test, else it is ignored and the view auto-scales — use this to safely fix a range computed for a specific test.
colorbarRangeMode 'spec' \| 'data' 'spec' Controls only the colorbar's numeric range when the active test has spec limits: 'spec' spans [limitLow, limitHigh]; 'data' spans the actual data min/max. In both ranges all dies are coloured by the gradient and out-of-spec dies are flagged with a triangle marker (▽ below limitLow, △ above limitHigh) over their gradient fill — so the distribution stays readable while out-of-spec dies remain visibly flagged. The marker is drawn black or white per die for contrast against its own gradient fill, so it stays visible under any colour scheme. Ignored under passFailDisplay: 'spec' (pass/fail mode always uses spec limits and fills dies solid green/blue/red).
logScale boolean from TestDef Override log₁₀ scale for the active test; falls back to linear when vMin ≤ 0
aggregationMethod string 'mean' Aggregation method in stackedValues mode: 'mean' | 'median' | 'stddev' | 'min' | 'max' | 'count'
rotation 0 \| 90 \| 180 \| 270 0 Clockwise rotation in degrees
flipX boolean false
flipY boolean false
showDieLabels boolean false Die index labels
showPartialDies boolean true Render partial (edge) dies in muted grey. No-op for a results-based map — those never have any (§12.1) — relevant only for a synthesized grid built via clipDiesToWafer.
showRingBoundaries boolean false
showQuadrantBoundaries boolean false
showReticle boolean false Reticle field boundary overlay (requires reticles on the result)
showXYIndicator boolean false Axis-orientation arrows showing +X/+Y directions
legendPosition 'default' \| 'compact' \| 'left' \| 'top' \| 'bottom' \| 'floating' 'default' Bin legend position. 'default' auto-adapts: compact below 280 px, floating below 180 px

Persisting user preferences

WaferViewOptions is the intersection of two named sub-types: - WaferPreferences — stable settings worth saving (bin and value colour schemes, the bin-definition colour toggle, rotation, overlays, legend position, log scale, colorbar range mode) - WaferDisplayState — transient session state (plot mode, active test, active metadata key, value range, highlight bin)

The onViewOptionsChange callback receives a category hint ('preference' | 'state' | 'mixed') so you can decide what to persist without filtering keys manually:

renderWaferMap(container, result, {
  onViewOptionsChange: (opts, changed, category) => {
    if (category === 'preference') saveToLocalStorage(opts);
  },
});

Use === 'preference' (not !== 'state') so that 'mixed' events — which may include transient fields like plotMode or activeTest — do not get written to storage.

5.2 Hover tooltip content by mode

Mode Tooltip content
value, hardBin, softBin Die (x, y) · one line per test value ("Idsat: 1.23 mA" with testDefs, "Test 1050: 1.23 mA" without) · bins with hard/soft labels
stackedValues Die (x, y) · test label + method + aggregated value (e.g. "Idsat (mean): 1.23 mA" with testDefs, "Test 1050 (mean): 1.23 mA" without)
stackedBins Die (x, y) · bin number · bin name · count · percentage (e.g. "1 · Pass: 3 (75%)")
stackedSoftBins Same as stackedBins but uses sbinDefs for name lookup

The aggregationMethod and lotSize fields on ViewOptions (§11, buildView) populate the method label and percentage denominator respectively. renderWaferMap and renderWaferGallery set both for you — a map built with lotStack carries its own lotSize.

5.3 Axis labels

When showAxes: true, tick labels show die grid indices (integer i/j values). renderWaferMap derives diePitchMm automatically from the view geometry, so axes always show grid indices. Only when calling toCanvas directly without supplying diePitchMm do axes fall back to mm values.

5.4 RenderOptions

These ToCanvasOptions fields (§9.1) are accepted: padding, background, showColorbar, colorbarWidth, showAxes, showTitle, legendOffset, diePitchMm and metadataFields. Plus:

{
  height?:                 number | string    // intrinsic map height. renderWaferMap fills its container, which must
                                            // therefore have a resolved height; set this and the library sizes its own
                                            // wrapper, so the map renders with no container CSS. Number = px, or any CSS
                                            // length ('600px', '70vh'). Omit when the container already has a height.
  showAxes?:               boolean            // draw axis tick marks and die grid index labels (default false)
  viewOptions?:           WaferViewOptions  // initial display state; plotMode, testDefs, and reticles are pre-seeded from the result automatically
  onHover?:                (die: Die | null, event: MouseEvent) => void
  onClick?:                (die: Die, event: MouseEvent) => void
  onSelect?:               (dies: Die[]) => void     // fires after box-select drag or click-select
  onViewOptionsChange?:   (opts: WaferViewOptions, changed: (keyof WaferViewOptions)[], category: 'preference' | 'state' | 'mixed') => void
                          // mirrors toolbar changes; changed lists the keys that were modified;
                          // category is 'preference' when all changed keys are WaferPreferences,
                          // 'state' when all are WaferDisplayState, 'mixed' when both
  showTooltip?:            boolean   // default true
  showToolbar?:            boolean   // default true
  showIdentity?:      boolean   // default true — the wafer's identity (lot, wafer ID, product, test
                                            // program, temperature, etc.) in the chrome row above the canvas, beside
                                            // the toolbar. Independent of showToolbar/Insights, and a CONTENT switch
                                            // only: the chrome row exists whenever there is a toolbar, so turning this
                                            // off costs the identity text and nothing else. Short metadata renders
                                            // inline when it fits; otherwise it collapses to one identifying line that
                                            // expands over the canvas on click/Enter/Space. Renders nothing when the
                                            // result has no metadata/lot-stack context.
  dieList?:                DieListDisplayOptions  // display preferences for the built-in die-list table (the
                                            // coordinate-less map replacement, and the "+N dies without position" footer)
                                            // — column selection, maxRows. See §5.4.1.
  showExpandButton?:       boolean   // show the expand button in the toolbar and enable the E-key shortcut
                                            // (default true). Independent of showIdentity — expand is a view control,
                                            // not part of the wafer's identity. Set false when the host already renders
                                            // the map inside its own expanded/modal context, where wmap's built-in
                                            // expand modal would be redundant
  statsSummary?:           StatsSummary  // precomputed wafer-level stats — adds a Summary toggle button to the toolbar
  summaryPanel?:           SummaryPanelOptions  // Summary panel placement and open/closed initial state
  insights?:               InsightsOptions  // adds an Insights toolbar button that swaps the map for this wafer's own
                                            // chart suite (Overview, Distributions, Correlation) — default disabled. See §5.9.
  warnings?:               WarningsOptions  // built-in surfacing of the library's own advisories — ON by default.
                                            // { display?: boolean; onWarning?: (w: WaferWarning[]) => void }
                                            // See §4.2.2 and the note below.
  showHelpButton?:         boolean   // show a help button in the toolbar that opens the built-in end-user guide
                                            // (default false); enable in applications that want to surface the guide
                                            // without linking externally. Opens as a real, separate window when
                                            // `window.open` is available, falling back to an in-page non-modal
                                            // floating window when it's blocked (some embedded WebViews — Tauri,
                                            // Electron, WebView2 — silently return null)
  userGuideExtension?:     UserGuideExtension  // insert a host app's own documentation into the guide window
                                            // (see "User guide extension" below) — only relevant when showHelpButton is true
  downloadFilename?:       string    // stem for the PNG download filename (default 'wafermap') — '.png' is appended automatically
  onSaveImage?:            (blob: Blob, suggestedName: string) => void | Promise<void>
                                            // host hook for persisting the rendered PNG. When provided, the toolbar's save
                                            // button calls it instead of triggering a browser <a download>, letting
                                            // embedded hosts (Tauri, Electron, WebView2) route the image through a native
                                            // dialog. When omitted, the default download behaviour is unchanged.
  onSaveText?:             (text: string, suggestedName: string, mimeType: string) => void | Promise<void>
                                            // host hook for every built-in "Export CSV" button — Summary/Insights test-values
                                            // and functional-tests tables, and the die-list table (§5.4.1). Mirrors
                                            // onSaveImage — when provided, called instead of a browser <a download> (a
                                            // silent no-op in Tauri/Electron/WebView2). When omitted, the default
                                            // download behaviour is unchanged.
  zIndex?:                 number    // base z-index for wmap's transient overlays (menus, tooltip, expand/help modals).
                                            // Omit for a safe high default (above typical app modal layers); set it to
                                            // embed the map inside your own modal/overlay. See "Overlay z-index" below.
}

Sizing. renderWaferMap fills its container, so the container must have a resolved height (width comes from document flow). A plain block <div> grows to fit the map; a flex/grid child needs a height-resolved parent or it collapses to zero — the library logs a warning when it detects this. Pass height to have the library size the container for you. See Troubleshooting → Map is blank, invisible, or the wrong height.

The box-select toolbar button is always shown. Providing onSelect lets your app react to selection changes; without it the selection is purely visual.

When statsSummary is provided, a summary panel toggle button (notebook icon) appears in the toolbar. The panel opens hidden by default; clicking the button shows or hides it. Clicking a finding in the panel highlights the affected die zone on the map.

Overlay z-index (zIndex / --wmap-z). Every transient overlay wmap creates in the host page — toolbar menus and dropdowns, the die hover tooltip, the expand modal, and the (non-modal) user-guide window — uses position: fixed. By default they stack at a high base value (6000), so they appear above typical app modal layers with no configuration. wmap layers its own overlays from the base upward: the modal backdrop and a component's own persistent chrome (e.g. a gallery card's toolbar) at the base, the tooltip and the modal box (over its own backdrop) at base + 1, every menu/dropdown/cascade-submenu at base + 2, and content that must clear a maximized modal box at base + 3. The user-guide window uses its own incrementing band above that, so it's never hidden behind a still-open modal. A gallery card detached into its own window (§6.6) is a separate OS window, not an in-page overlay, so none of this stacking applies to it — it's positioned and raised by the OS window manager instead.

Menus specifically render into one dedicated layer per overlay root (see "Menus opened from inside..." below) rather than at the same tier as ordinary persistent chrome — a persistent, host-page-pinned element (your own sticky header, a docked panel) can sit anywhere in your stacking order without ever being able to outrank a wmap menu, because menus don't compete on the general base/tooltip/modal scale at all.

To embed a wmap render inside your own modal or overlay, pass zIndex so wmap's menus and tooltips land above it:

// Host modal at z-index 5000; put wmap's overlays above it:
renderWaferMap(container, result, { zIndex: 5100 });

zIndex is applied for the lifetime of the render and restored on controller.destroy(). Internally it writes the --wmap-z CSS custom property on document.documentElement (overlays that append to document.body inherit it from there, and every tier above — including the menu layer — is itself calc(var(--wmap-z, 6000) + N), so raising zIndex shifts the whole stack together); you can set --wmap-z yourself instead of passing zIndex if you prefer to control stacking via CSS. Menus opened from inside the expand modal or the user-guide window are appended to that box's own menu layer rather than document.body's, so they always appear above its content regardless of the host page's stacking context.

Native <dialog> host modals need no configuration at all. If your own modal is built on the native <dialog> element and shown via .showModal(), it's promoted into the browser's top layer — a rendering layer that sits above the entire normal stacking order unconditionally, regardless of any z-index (no zIndex value, however high, can make a document.body-level element paint above it). wmap detects this automatically: every overlay it creates (tooltip, menus, the expand modal, the user-guide window's in-page fallback) checks whether its trigger lives inside a <dialog> currently shown modally (element.matches(':modal')) and, if so, roots itself inside that dialog instead of document.body — so it lands correctly in the same top-layer subtree with no zIndex or other configuration needed. This is unrelated to the zIndex mechanism above, which only matters for ordinary (non-<dialog>) modal implementations — a plain fixed/absolute overlay div, whatever library built it.

5.4.1 Theming (--wmap-* custom properties)

wmap's chrome — the toolbar, summary panel, menus, the die tooltip, and the wafer canvas itself (background, axis labels, grid) — is themed entirely through CSS custom properties. Set them on any ancestor of the render container and everything wmap draws follows. Every token has a light default baked in, so if you set nothing you get the default light appearance; override only what you want.

This is the same mechanism --wmap-z uses for stacking. It is theme-agnostic: wmap defines the tokens and their defaults, the host supplies the values. (The data palette — the bin/value colours of the dies — is separate and controlled by colorScheme, not these tokens; it does not follow the chrome accent.)

Type size (--wmap-font-size)

wmap's chrome is sized from one token. Set --wmap-font-size and every tier moves with it, so an embedded map follows the host's own type scale instead of pinning wmap's:

:root { --wmap-font-size: 14px; }   /* default 12px */

The tiers are derived, not individually settable — deliberately, so a host cannot produce an incoherent scale (headings smaller than body, meta larger than headings):

Tier Size Used for
meta base − 1px uppercase micro-labels, badges, disclosure arrows
body base prose, table cells, hints, and every interactive control
sub-heading base + 1px group headers inside a scrolling list
heading base + 3px card and panel titles
stat base + 8px the large summary figures

Canvas text follows too, but resolves the token at paint time from the document root — canvas cannot read a CSS variable. Set --wmap-font-size on :root (as with the colour tokens) rather than scoping it to a container, or chart and axis labels will keep the default while the DOM chrome moves.

Density (--wmap-density)

The companion lever to --wmap-font-size. It scales every step of the spacing scale, so an embedded map can tighten into a narrow column or breathe in a roomy host:

:root { --wmap-density: 0.85; }   /* default 1 */

Type is deliberately not affected. Density is the space between things; shrinking type to fit a column is what produced a segmented toggle that rendered smaller than the buttons beside it. Corner radius is likewise unaffected — it is three fixed roles (control 4px, container 6px, pill full).

Typeface (--wmap-font-family)

The third sizing lever. It defaults to inherit, so an embedded map picks up the host's own typeface without being told; set it only when the map should differ from its container:

:root { --wmap-font-family: "IBM Plex Sans", system-ui, sans-serif; }  /* default: inherit */

Like --wmap-font-size, canvas text cannot read the variable, so it reaches DOM chrome only — set a stack the canvas defaults sit comfortably beside rather than a display face.

Token reference (default in parentheses):

Token Themes Default
--wmap-font-family DOM-chrome typeface (canvas text is unaffected) inherit
--wmap-canvas-bg Wafer canvas background #f5f5f5 (falls back to --wmap-surface)
--wmap-surface Menus, panels, toolbar surfaces, gallery cards #fff
--wmap-panel-bg Summary-panel base #fafbfc
--wmap-border Borders, dividers, axis tick lines rgba(0,0,0,0.12)
--wmap-control-border Button, toggle and input edges — deliberately stronger than --wmap-border, which is a hairline divider rgba(0,0,0,0.30)
--wmap-text Primary text (chrome + canvas axis/legend) #333
--wmap-text-muted Secondary/muted text #66788a
--wmap-text-strong Emphasis text — Summary-panel headings, big stat numbers, metadata badge values #1f2f43
--wmap-icon Toolbar icon default #506784
--wmap-icon-hover Toolbar icon hover #2a3f5f
--wmap-icon-active Active icon / on-canvas accent #1a66cc
--wmap-bg-hover / --wmap-bg-active Hover / active row backgrounds #edf0f8 / #dce8f8
--wmap-menu-hover / --wmap-menu-active Menu-item hover / active #f0f4fc / #dce8f8
--wmap-separator Faint separators rgba(0,0,0,0.12)
--wmap-warn-bg / --wmap-warn-border / --wmap-warn-text Warning banner #fffbe6 / #f0c040 / #7a5800
--wmap-err-bg / --wmap-err-border / --wmap-err-text Error banner (geometry advisories — dies may be mis-positioned) #fef2f2 / #dc8a8a / #7a1c1c
--wmap-info-bg / --wmap-info-text Info callout #dce8f8 / #334155
--wmap-selected Finding-drilldown card outline (gallery) #e07a20
--wmap-finding-indicator Summary button text colour when the wafer/lot has notable findings #b7551a
--wmap-bar-fill Summary-panel progress bars (wafer yield, region yield, bin breakdown) — fill #2a6fc0

--wmap-err-* and --wmap-warn-* are visually distinct on purpose — a warning says something is missing or degraded, an error says the map may be positionally wrong (geometry advisories), and flattening the two into one colour hides the difference that matters. Every token that pairs a background with text on it — warn-*, err-*, text-strong against panel-bg/surface — needs its own AA-contrasting pair when you theme it. Overriding only the surfaces and leaving these unset does not make them invisible; it makes them fall back to the light-theme defaults above, which is how a dark theme silently ends up with near-black text on a near-black panel. §5.4.1's dark/Nord examples below set all of them for exactly this reason.

Canvas colours are resolved from these variables at draw time and re-resolved on a theme change or OS light/dark flip, so the wafer repaints to match.

Following the OS light/dark preference — put the light values on :root and override in a media query:

:root {
  --wmap-surface: #fff;
  --wmap-text: #333;
  /* …other light values… */
}
@media (prefers-color-scheme: dark) {
  :root {
    --wmap-canvas-bg: #242426;
    --wmap-surface:   #2a2a2d;
    --wmap-panel-bg:  #202022;
    --wmap-border:    #3a3a3a;
    --wmap-text:      #ccc;
    --wmap-text-muted:#888;
    --wmap-icon:      #aaa;
    --wmap-icon-hover:#6af;
    --wmap-icon-active:#6af;
    /* …etc… */
  }
}

Example — a dark theme (drop-in; overrides only what differs from the light defaults):

.wmap-dark {
  --wmap-canvas-bg:   #242426;
  --wmap-surface:     #2a2a2d;
  --wmap-panel-bg:    #202022;
  --wmap-border:      #3a3a3a;
  --wmap-text:        #ccc;
  --wmap-text-muted:  #888;
  --wmap-text-strong: #f2f2f2;
  --wmap-icon:        #aaa;
  --wmap-icon-hover:  #6af;
  --wmap-icon-active: #6af;
  --wmap-bg-hover:    #343438;
  --wmap-bg-active:   #2b3a4f;
  --wmap-menu-hover:  #343438;
  --wmap-menu-active: #2b3a4f;
  --wmap-separator:   #2a2a2a;
  --wmap-warn-bg:     #3a2f0f;
  --wmap-warn-border: #7a6222;
  --wmap-warn-text:   #f0c04d;
  --wmap-err-bg:      #3a1414;
  --wmap-err-border:  #7a3030;
  --wmap-err-text:    #ff8a8a;
  --wmap-info-bg:     #1a2b3d;
  --wmap-info-text:   #8ecbff;
  --wmap-selected:    #6af;
  --wmap-finding-indicator: #ffa057;
}

Every one of these matters: the six tokens that used to be left unset here — --wmap-text-strong plus the warn-*/err-*/info-*/finding-indicator group — do not go unstyled when omitted. They fall back to wmap's light-theme defaults, and --wmap-text-strong (the Summary panel's heading/big-number colour) defaults to #1f2f43, near-black. Near-black text on the #202022 panel above is under 1.1:1 contrast — invisible, not merely dim. Omitting any of this block's tokens reproduces that bug for whichever surface reads the one you left out.

Example — Nord (a branded palette; shows the same structure with a different accent):

.wmap-nord {
  --wmap-canvas-bg:   #2e3440;
  --wmap-surface:     #323846;
  --wmap-panel-bg:    #2b303b;
  --wmap-border:      #434c5e;
  --wmap-text:        #e5e9f0;
  --wmap-text-muted:  #a6adbb;
  --wmap-text-strong: #eceff4;
  --wmap-icon:        #d8dee9;
  --wmap-icon-hover:  #88c0d0;
  --wmap-icon-active: #88c0d0;
  --wmap-bg-hover:    #3b4252;
  --wmap-bg-active:   #3b4a58;
  --wmap-menu-hover:  #3b4252;
  --wmap-menu-active: #3b4a58;
  --wmap-separator:   #3b4252;
  --wmap-warn-bg:     #3b3220;
  --wmap-warn-border: #7a6a3a;
  --wmap-warn-text:   #ebcb8b;
  --wmap-err-bg:      #3b2020;
  --wmap-err-border:  #7a4040;
  --wmap-err-text:    #f39a9a;
  --wmap-info-bg:     #20303a;
  --wmap-info-text:   #88c0d0;
  --wmap-selected:    #88c0d0;
  --wmap-finding-indicator: #dc9a80;
}

The reference host tsmap implements a full theme picker (light, dark, Nord, Solarized, a brand green, high-contrast) this way — its index.html :root blocks are a worked example of the complete token set across multiple themes.

5.4.2 SummaryPanelOptions

{
  placement?:   'right' | 'left' | 'top' | 'bottom'  // panel side; default 'right'
  defaultOpen?: boolean                               // open on mount; default false
}

The Summary panel is a docked panel — metadata, yield, detected anomalies (StatsSummary.findings, with severity chips wired to filterFindings, §7.x; the Kind/Region dropdowns appear once there are at least 8 findings), bin breakdown, region yield, test values, and functional tests — plus one combined "Summary report" button that opens the full HTML report (renderSummaryReportHtml/renderLotSummaryReportHtml, which already includes findings) in an in-app modal (openReportModal, §10.3) — no host wiring required. Always co-visible with the map so a clicked finding can highlight the affected dies right there — see §5.9 for why this is a separate surface from Insights. Its bin/region/test-value numbers and Insights' Overview sub-tab read the same underlying computation (StatsSummary.stats.*, buildRegionYieldData, buildCapabilityData), so the two surfaces can show overlapping numbers without ever disagreeing.

Findings render directly beneath the headline stats, above the bin/region/test detail. Every section collapses from its header, and the collapsed set is remembered per panel element across re-renders.

The gallery's panel has no tabs. It previously opened on a Lot/Findings pair in which both tabs carried findings — lot-level ones under "Lot", and none at all under "Findings", which actually listed wafers — and in which two identical-looking per-wafer lists did different things on click. There is now one list: the Wafer Yield section, with each row badged by its own findings count and opening that wafer when clicked. Every wafer appears in it, including those with no findings, which the old subset list structurally could not show. A Findings report button beside the other report buttons covers every wafer's findings in one document.

On the single-wafer panel the metadata section is suppressed when the caller already renders that metadata — renderWaferMap does so whenever its identity header is mounted (showIdentity, default true), since the header's expandable panel is built from the same helpers. Set showIdentity: false and the panel's own "Wafer Info" section returns.

Three sections carry a header selector, and each derives its default rather than starting neutral:

Section Selector Default
Bin breakdown Hard / Soft The map's plot mode (hardBin/stackedBins → hard, softBin/stackedSoftBins → soft), falling back to whichever bin type has data. Shown only when both types have data.
Region yield Ring / Quadrant Ring. Quadrant yield averages over half the wafer and is near-flat on most lots; a real asymmetry is reported as a finding with a significance test behind it.
Wafer yield (lot) Slot / Yield Slot order, which is what makes a slot-correlated pattern visible.

Bin bars are ordered pass-bins-first, then failing bins by descending count — matching buildBinParetoData's count-descending ordering in the Insights bin chart — and are labelled "% of dies (N=…)" to distinguish them from the lot card's "Mean wafer yield", which is an unweighted mean of per-wafer yields. The two are different statistics over different denominators and agree only when die counts are even across the lot.

The on-screen test table carries Test / Mean / Ppk / Spec yield only; the full descriptive statistics (min, quartiles, median, max, σ, both limits) stay in the CSV export and the summary report. Ppk rather than Cpk because Cp/Cpk use the pooled within-wafer stddev — on a single-wafer panel there is exactly one subgroup, so cpk === ppk identically and the "Cpk" label would name an index the data does not contain; across a lot, Cpk excludes the wafer-to-wafer shift that Ppk includes. The Cpk/Ppk pair is a drift diagnostic and lives in the summary report, which prints all four indices. A test's N is hoisted into the section title when every test shares it.

5.4.3 InsightsOptions

{
  enabled?:     boolean                                          // show the Insights toolbar button; default false
  defaultView?: 'overview' | 'distributions' | 'correlation'      // sub-tab shown first; default 'overview'
  defaultOpen?: boolean                                          // open Insights on mount instead of the map; default false
}

defaultOpen is for an analysis-first surface, where the charts are the point and the map is the secondary view — symmetric with summaryPanel.defaultOpen. Note that the chart suite is a lazily-imported chunk, so opening it on mount also pulls that chunk on load rather than on first click; leave it off for a map-first page. See the Insights example, which uses it because the charts are its whole subject.

5.4.4 Die list & CSV export

Calling buildDieListSection directly is deprecated — removed in 0.31.0. The maps show the die list themselves (the dieList option).

buildDieListSection (@wafertools/wafermap/render) is the general "show me the raw dies" table — one row per die, with an Export CSV button. It backs three built-in surfaces — the coordinate-less map replacement, the "+N dies without position" footer, and the "View die list" Summary panel link, below — and is also exported for standalone use.

"View die list" is a link inside the always-available Summary panel (§5.4.2/§6.5), not a new toolbar button — deliberately, since the toolbar already carries a dozen buttons and this reuses an existing entry point the same way "Summary report" opens the HTML report without one either. On by default whenever a Summary panel is reachable at all — set enabled: false to hide it:

renderWaferMap(container, result, {
  summaryPanel: {},   // the panel this link lives inside — "View die list" appears automatically
});

renderWaferMap(container, result, {
  summaryPanel: {},
  dieList: { enabled: false },   // …unless you don't want it
});

On renderWaferMap it opens this wafer's own dies. On renderWaferGallery (§6.5) the equivalent GalleryOptions.dieList opens every wafer in the lot, pooled, with a leading Wafer column (one per die, resolved from each card's own label) and wafer-metadata columns computed via commonMetadata — a field common to every wafer appears once; a field that varies across the lot (a mixed-lot pool) does not appear at all, rather than printing one wafer's value as if it applied to the whole export. Both open the same resizable modal wmap's toolbar already uses elsewhere (chart drilldown, wafer expand), with buildDieListSection mounted inside it — same metadata columns, same maxRows, same CSV export as every other die-list surface, all driven by the one shared DieListDisplayOptions object.

import { buildDieListSection } from '@wafertools/wafermap/render';

buildDieListSection(dies, testDefs, options?);   // → HTMLDivElement | null

Columns, in order: [extraColumn?] → X, Y [→ Ring, Quadrant] [→ Edge excluded] → Site, Hard bin, Soft bin → one column per test → die metadata → wafer metadata.

X/Y are separate numeric columns, not a single "(x, y)" cell — a bracketed pair reads fine on screen but forces an extra parsing step (or breaks outright) when the CSV is opened in Excel, pandas, or any other tool expecting one number per cell. Blank for an unpositioned die.

Ring/Quadrant appear only when getWafer resolves at least one die to a Wafer — omitted entirely otherwise, not shown empty. Values match classifyDie (§7) exactly, using the same ringCount as the rest of this wafer/lot's analysis, so "Ring 2" here always names the same region a Ring finding does. Reached through renderWaferMap/renderWaferGallery, this is wired up automatically — a direct buildDieListSection call supplies getWafer itself.

Edge excluded appears only when at least one die in the export has edgeExcluded: truebuildWaferMap's edge exclusion (waferConfig.edgeExclusion) is opt-in and only ever stamps true on the dies it excludes, never false on the ones it doesn't, so "no die is excluded" and "the feature was never configured" are indistinguishable from Die[] alone; omitting the column in that case is the closest available approximation. When shown, Yes/No is unambiguous for every row, since at least one exclusion is known to have happened somewhere in the export.

Metadata columns are on by default (metadataColumns: 'auto') — every key found on any die's DieMetadata, deterministically ordered (metadataFields declaration order first, then the rest naturally sorted). Unlike the 'metadata' plot mode, where metadataFields gates which keys are offered (a legend has a cardinality limit), a table column has none — an export silently dropping host data would be worse than a wide one. Pass an explicit string[] to pin the set and order, or 'none' to omit die metadata entirely.

Wafer metadata (lot, waferId, product, …) is CSV-only by default (waferMetadataColumns: 'csv') — constant down every row, so it is noise on screen next to the always-visible metadata badge (§5.4), but it is exactly what makes a detached CSV self-describing enough to concatenate several exports and still know which wafer each row came from. Set 'both' to also show it in the table, or 'none' to omit it.

A key present on both the wafer and a die produces exactly one column, scope die, carrying the die's value — the same shadowing rule the hover tooltip (§12.4) applies.

Column labels resolve metadataFields[].labelprettyKey(key) → the raw key, matching every other column's human-readable header. A metadata key that collides with a built-in column name (e.g. a key literally called Site) is never dropped — it becomes "Site (metadata)", or falls back further to "Site (site)" and then a numbered suffix in the (rare) case that also collides.

maxRows (default 50_000) caps how many rows are built as real DOM — this table has no virtualisation, so an uncapped multi-hundred-thousand-die lot is a genuinely slow, heavy build. The CSV export is never capped; it always contains every die, and a footer states the truncation explicitly ("Showing the first 50,000 of 266,412 dies. The CSV export contains all 266,412.") whenever it applies. Set maxRows: 0 to skip the table and offer only the CSV export.

export interface DieListDisplayOptions {
  metadataColumns?:      'auto' | 'none' | string[];   // default 'auto'
  waferMetadataColumns?: 'csv' | 'both' | 'none';        // default 'csv'
  maxRows?:               number;                        // default 50_000; CSV is never capped
  maxHeight?:             string;
}

export interface DieListOptions extends DieListDisplayOptions {
  title?:          string;
  note?:           string;
  onSaveText?:     SaveTextHandler;
  extraColumn?:    { label: string; get: (die: Die) => string | undefined };
  getWafer?:       (die: Die) => Wafer | undefined;  // per-die wafer for Ring/Quadrant — see below
  ringCount?:      number;              // default 4 — must match this wafer/lot's own ringCount
  waferMetadata?:  WaferMetadata;       // e.g. WaferMapResult.metadata
  metadataFields?: MetadataFieldDef[];  // e.g. WaferMapResult.metadataFields
  ownerDocument?:  Document;            // default `document` — see below
}

When reached through renderWaferMap's RenderOptions.dieList, waferMetadata and metadataFields are always supplied by the library from the current build result — never from the host option — so a standalone-caller mistake can't substitute the wrong identity data into an export. A direct buildDieListSection call supplies both itself.

ownerDocument only matters for a host that might mount the table into a document other than the bare global — e.g. a popup window it opened itself. It's irrelevant for ordinary in-page use, including everywhere the library reaches this internally (the Summary panel's "View die list" link, the coordinate-less-wafer map replacement).

5.5 WaferMapController

Choose the right update method: - setResult — new wafer loaded (different geometry, dies, and/or test data). Re-seeds bin defs, testDefs, and reticles from the new result automatically. - setOptions — display-only change: plot mode, colour scheme, zoom, etc. No data reload.

{
  setResult(result: WaferMapResult): void            // replace wafer geometry and die data
  setOptions(opts: Partial<WaferViewOptions>): void // merge options, rebuild view
  getOptions(): WaferViewOptions                    // current options snapshot
  setSelection(dies: Die[]): void                    // programmatically highlight dies
  clearSelection(): void
  resetZoom(): void                                  // return to fitted view
  setStatsSummary(summary: StatsSummary | undefined): void  // update the Summary panel at runtime

  setInsightsOpen(open: boolean): void  // programmatically open/close the Insights tab; no-op if `insights.enabled` was not set

  openUserGuide(): void   // opens the end-user guide window directly — the same action the help toolbar button
                                  // performs, but callable regardless of showHelpButton, so a host
                                  // that hides wmap's own help button (e.g. folding it into its own combined help menu)
                                  // can still trigger the guide without a DOM query

  destroy(): void                                    // remove all listeners and DOM elements
}

5.6 Toolbar buttons (full mode)

Button Action
Camera Export current view as PNG
Zoom region Drag to draw a zoom rectangle
Pan Drag to pan the map (default mode)
Box select Draw selection rectangle — fires onSelect callback if provided
Zoom + Zoom in centred on canvas
Zoom − Zoom out centred on canvas
Reset Return to fitted view (also: double-click canvas)
Mode Grouped dropdown: Test Value section (one entry per test — labelled by testDef.name when provided, otherwise Test {N} using the testNumber; cascade submenu when > 6 tests) · Bins section (Hard Bin, Soft Bin) · Lot Aggregation section (Stacked Test Values, Stacked Hard Bins, Stacked Soft Bins). Only modes for which data is actually present are shown.
Palette Menu: in Hard/Soft Bin mode, the registered bin colour schemes (plus Use colours from bin definitions when a BinDef carries a colour); in every other mode, the registered value gradients. Hidden in 'metadata' mode.
Log scale Toggle log₁₀ scale for the colorbar and value normalization. Shown only in value / stackedValues modes, and hidden (not just dimmed) whenever a solid pass/fail display is active or the active test is functional, since log scale has no effect on pass/fail colouring. Overrides the per-test TestDef.logScale default. Silently falls back to linear when vMin ≤ 0.
Colorbar range Toggle colorbar range between spec ([limitLow, limitHigh]) and data (actual min/max). Only shown in value mode when the active testDef has at least one limit defined. Active (highlighted) = spec range; inactive = data range. In both states all dies keep the gradient fill and out-of-spec dies are flagged with a triangle marker (▽ below limitLow, △ above limitHigh) over that fill.
Rings Toggle ring boundary overlay
Quadrants Toggle quadrant boundary overlay
Labels Toggle die index text labels
Reticle Toggle reticle field overlay — only shown when reticles are present
XY indicator Toggle axis-orientation arrows showing +X/+Y directions
Legend style Dropdown: bin legend position — Default (right), Compact (right), Left, Top, Bottom, Floating (draggable). Disabled outside hardBin/softBin/metadata modes.
Rotate Rotate 90° clockwise (cycles 0→90→180→270)
Flip H Mirror horizontally
Flip V Mirror vertically
Summary Toggle the Summary panel — only shown when statsSummary is provided
Insights Toggle the Insights tab — swaps the map for this wafer's chart suite. Only shown when insights.enabled: true. See §5.9.
User guide Open the built-in end-user guide — a real, separate window when available, falling back to an in-page non-modal floating window when window.open is blocked (some embedded WebViews). Only shown when showHelpButton: true; callable directly via openUserGuide() regardless.

Expand opens the map in an enlarged modal overlay; the map box is reparented — no view rebuild. A maximise button in the modal grows it to fill the window (F). Close with Esc, the × button, or the backdrop. Keyboard shortcut: E. It works in the Insights view too, where it expands the whole chart suite into a wide modal — those charts interact, and reading them side by side is the case the modal exists for; individual chart panels keep their own expand button for enlarging just one. Hidden inside gallery cards (which have their own non-modal expand, see §6) and inside an already-open modal or window.

While the Insights tab is open, every control above except Insights and User guide is hidden — Camera/Zoom/Pan/Box select, Mode/Palette/Log scale/Colorbar range/Rings/Quadrants/Labels/Reticle/XY indicator/Legend style/Rotate/Flip, and Summary all apply only to the map view, which the chart suite has replaced; Summary's panel specifically would have nothing to highlight against with the map hidden behind Insights. Expand is likewise hidden while Insights is open, for the same reason. They reappear as soon as Insights is closed.

5.7 Interactions

Gesture Mode Action
Scroll wheel Zoom mode Zoom in/out centred on cursor
Drag Pan mode (default) Pan the map
Drag Zoom mode Draw zoom rectangle
Drag Select mode Box-select dies
Click on die Any onClick callback; selects die if onSelect provided
Ctrl/Cmd+click Any Toggle die in/out of selection
Ctrl/Cmd+drag Select mode Additive box-select
Hover over die Any Tooltip + onHover callback
Click bin legend entry Any Toggle highlightBin — dims all non-matching bins
Double-click Any Reset to fitted view
Esc Any Clear selection; also closes the expand modal
E key Any (focus on canvas) Open / close the expand modal

Note: zoom/rotate/flip are visual-only transforms — they never mutate the underlying Die data. Selection stability is guaranteed: die.x and die.y remain unchanged regardless of display orientation.

5.8 Example usage

import { buildWaferMap } from '@wafertools/wafermap';
import { renderWaferMap } from '@wafertools/wafermap/render';

const result = buildWaferMap({ results, waferConfig, dieConfig });

const ctrl = renderWaferMap(document.getElementById('map'), result, {
  viewOptions: { plotMode: 'hardBin', binColorScheme: 'accessible' },
  onClick:  (die)  => console.log(die.x, die.y, die.hbin, die.sbin),
  onSelect: (dies) => console.log(`Selected ${dies.length} dies`),
  onViewOptionsChange: (opts, changed, category) => {
    if (category === 'preference') savePreferences(opts);
    syncExternalUI(opts, changed);
  },
});

// Replace wafer geometry and die data after a full data reload:
ctrl.setResult(newResult);

// Programmatically change display mode:
ctrl.setOptions({ plotMode: 'value', valueColorScheme: 'plasma' });

// Clean up:
ctrl.destroy();

5.9 Insights tab

Passing insights: { enabled: true } adds an Insights toolbar button. Clicking it swaps the map for a chart suite computed from this wafer's own dies — the same panels a gallery's Insights tab shows (§6.10), scoped to one wafer. Clicking the button again (or the toolbar's Insights button) returns to the map view; the toolbar itself stays visible and usable the whole time so the Insights button is always reachable to close the tab.

Separate from the Summary panel (§5.4.2) on purpose. A finding's entire value is click-to-highlight-on-map, which can't work inside a full takeover of the map — so the Summary panel (which includes findings) stays docked, always co-visible with the map, while Insights takes over the full view for chart-heavy content that doesn't reference specific dies. The two toggle independently; opening one never hides the other's toolbar button. Insights' Overview numbers and the Summary panel's compact bin/ring/quadrant/test-value rows read the same underlying computation, so they never disagree even though both can be on screen in principle.

Insights has three sub-tabs:

  • Overview — a per-test pass rate chart (worst test first, clustered by group when "Group by" is active), headline tiles naming the population (wafer count, dies analysed and excluded, and for a lot the mean wafer yield, labelled unweighted, per wafer to distinguish it from the die-weighted figure), a yield bar (labelled with the actual passBins in use, e.g. "Yield by wafer (pass: bin 1)", with a dashed median reference line), a hard/soft bin pareto, and a details card with ring/quadrant regional yield and the full per-test statistics table. The pass-rate chart has three modes (buildTestPassRateData, @wafertools/wafermap/stats), because a parametric test carries two independent pass/fail notions and a functional test only one:
Mode Judged by
spec The value against limitLow/limitHigh.
testFlag The tester's own recorded verdict in die.testPassSTDF's PTR TEST_FLG pass/fail bits, which exist whether or not LO_LIMIT/HI_LIMIT do.
functional The recorded verdict for a testType: 'F' test, which has no measured value and therefore only ever has this one.

The two parametric modes can legitimately disagree — guard bands, dynamic or per-site limits, criteria the exported limits do not describe, or a limits/data mismatch. They are therefore kept as separate views rather than collapsed into one "parametric pass rate", and TestPassRateData.disagreementDies counts the dies the two sources judge differently (null, distinct from 0, when only one source is present). The chart surfaces that count rather than resolving it, since only the reader can tell an expected guard band from a real mismatch.

Only the modes the data supports are offered; hasJudgeableTests(testDefs, kind, dies) takes the dies for 'testFlag', since every parametric test could carry a verdict and a definition-only check would offer a mode that renders empty. Bars are rates on a fixed 0–100% axis rather than counts, so splits with different wafer counts compare fairly. A parametric test with neither limits nor a recorded verdict is genuinely unjudgeable and is omitted rather than reported as 100%. - Distributions — process capability (Cp/Cpk/Pp/Ppk, normalized per test — see §7.16's buildCapabilityData for how tests without full spec limits are handled), a test-value boxplot, a value histogram, and a wafer-to-wafer trend (per-wafer mean with ±1σ whiskers, the die-weighted lot mean as a dashed centre line, and spec limits). The trend is always in slot order and has no sort control: a drift or a bad cassette position is only visible in the physical sequence, so sorting it would remove the only signal it carries. - Correlation — a Pearson-r correlation matrix (stating the median pairwise n, with the exact per-pair n in each cell's tooltip) and a die-level X/Y scatter that reports r and n for the pair it is showing, recomputed when the legend filters the points. Clicking a capability box drives the boxplot, histogram and trend onto that same test in place; clicking a correlation matrix cell drives the scatter panel's X/Y in place.

For a single wafer there is no "Group by" control (grouping needs more than one wafer to be meaningful — see §6.10) and no click-to-open-wafer action (the map you're looking at already is the only wafer there is to open). Everything else — the wafer picker on histogram/correlation/scatter, the capability↔boxplot/histogram cross-link, the correlation↔scatter cross-link — behaves the same as the gallery version.

insights.enabled only changes what the toolbar exposes; it needs no other options.

The chart suite is loaded on demand. It is a separate chunk (~25 KB gzipped), fetched the first time Insights is opened and never downloaded by a page that only renders maps — the same treatment the in-app user guide gets. Two consequences worth knowing:

  • WaferMapController.setInsightsOpen(true) returns before the tab's DOM exists. The toolbar responds immediately, but code that asserts on the chart DOM straight after the call must wait for it to appear — polling for button[role="tab"] is the cheapest reliable signal. Closing is synchronous, and toggling back to the map while the chunk is still in flight is honoured rather than overridden.
  • A host that bundles wmap itself needs a bundler that supports dynamic import() code-splitting (Vite, Rollup, webpack, esbuild with splitting: true — all of them by default). One that inlines dynamic imports still works; it simply loses the saving. insights.defaultView picks which sub-tab shows first (default 'overview'). Panels that read parametric test data (capability, boxplot, histogram, trend, correlation, scatter, and the pass-rate chart's spec/testFlag modes) need testDefs passed to buildWaferMap to have anything to plot — yield and bin pareto only need die.hbin/die.sbin.
const result = buildWaferMap({ results, waferConfig, dieConfig, testDefs, passBins: [1] });
renderWaferMap(document.getElementById('map'), result, { insights: { enabled: true } });

5.10 Warnings

renderWaferMap and renderWaferGallery surface the library's own advisories themselves. A ⚠ indicator appears in the toolbar only when there is something to say; clicking it lists each advisory with its code and explanation. The same set feeds the Summary panel's banner.

This is on by default and needs no wiring. The reasoning: the library raises advisories that mean the map may be positionally wrong (partial-coverage and friends — §4.2.2), and it is the only party that knows. Leaving it to the caller to notice and display them means, in practice, that nobody is told.

It is deliberately not a toast. These are persistent conditions about whether the map can be trusted, and a message that dismisses itself leaves the map still wrong with no way back to the explanation.

interface WarningsOptions {
  display?:   boolean                              // default true
  onWarning?: (warnings: WaferWarning[]) => void   // collected, de-duplicated, severity-ordered
}

Hosts with their own notification system should pass both:

renderWaferMap(el, result, {
  warnings: {
    display: false,                    // suppress the built-in indicator
    onWarning: (warnings) => myToastSystem.show(warnings),
  },
});

The library still does the collecting, de-duplicating and severity ordering; the host owns only presentation. onWarning fires once on mount — with an empty array when there is nothing to report, so you can clear your own display — and again whenever the set changes (for example when a statsSummary arrives later and raises the test-count cap).

Turning display off without wiring onWarning means nobody is told, which is the situation this feature exists to end.

Collecting without a map. collectWarnings and severityOf are exported from @wafertools/wafermap/render, so a host can reproduce exactly the set the built-in UI would show rather than re-deriving it from two separate sources:

import { collectWarnings } from '@wafertools/wafermap/render';

const warnings = collectWarnings({ result, statsSummary });   // most severe first

Warnings are de-duplicated on code + message. A lot legitimately raises the same geometry advisory on every wafer; repeating it twenty times would bury the one that differs.

5.11 User guide extension

showHelpButton: true (§5.4, §6.2) opens wmap's own built-in end-user guide — but a host application often has its own documentation too, and forcing the user to find two separate help buttons/documents is poor UX. userGuideExtension inserts the host's own content before wmap's guide content in the same window, so there's one help button and one combined document instead of two.

A host's own help entry point often needs to work before anything is rendered too (an empty-state "Help" menu, say) — renderWaferMap/renderWaferGallery's userGuideExtension option only takes effect once one of them has actually been called. For that gap, openWaferMapGuide (§10.4) opens the identical combined window directly, no live render required.

import type { UserGuideExtension } from '@wafertools/wafermap/render';

interface UserGuideExtension {
  html:   string   // host-provided HTML, inserted before wmap's own guide content.
                    // Static content only — must not contain <script> tags (see below).
  title?: string    // overrides the floating window's title bar text
                    // (default 'Wafer Map — User Guide')
}
renderWaferMap(container, result, {
  showHelpButton: true,
  userGuideExtension: {
    title: 'My App — Help',
    html: '<h1>My App</h1><p>App-specific documentation goes here…</p>',
  },
});

wmap's own guide content keeps its own <h1>Wafer Map — User Guide</h1> further down the page — the result reads as one combined document with the host's section first, not a title rewrite of wmap's own content. title, when given, only affects the window chrome (title bar), matching the same convention renderWaferGallery's window uses.

No <script> tags in html. wmap re-executes exactly one inline script after inserting the combined content — its own live-demo bootstrap, always the last element in wmap's guide HTML — by finding the content container's first <script> in document order. A <script> tag in the host's own HTML would be found first instead, silently breaking wmap's live demos. Keep html to static markup (headings, prose, images, links).

Printing. When the guide opens as an in-page overlay (any host where window.open is blocked or unavailable, e.g. Tauri/Electron/WebView2), its header includes a Print/Save-as-PDF button that calls the host page's own window.print() — no host wiring needed. A real separate popup (plain browser hosts, where window.open succeeds) needs no such button: it's an actual OS window with nothing else on the page to exclude, so the browser's native Ctrl+P/Cmd+P already prints just the guide.

Combined "Contents" navigation. A sticky bar at the top of the guide window lists every <h2 id="..."> found in the combined document — the host's extension.html and wmap's own guide content alike — behind a "Contents" disclosure toggle, plus a "Top" shortcut. This is what makes the combined window read as one guide rather than two stacked documents: without it there is no shared navigation between a host's own sections and wmap's, and no way back to an earlier section once a reader has scrolled past it. Every heading in the combined document also gets a scroll-margin-top so a jump target never lands hidden underneath the sticky bar — this applies to a host's own internal cross-references too (any anchor link inside extension.html), not just this nav's own links. Give every section-level heading in html a real, unique id (the pattern in the example above, using <h2 id="..."> for section headings, is what the nav looks for) and it is picked up automatically — there is nothing else to opt into. Appears once there are at least two <h2 id> elements total; wmap's own guide alone already clears that, so every host gets this nav, whether or not it supplies an extension.

The list is deliberately one flat list, not grouped or split by source — a host's help button opens what reads as one guide, and a reader has no reason to know or care that it's actually two source documents stitched together. Each side's <h2>s are typically numbered from their own source markdown independently (1., 2., 3. …), which would collide head-on partway through this one list, so the nav strips any leading N. a heading's own text supplies and lets position in the list carry the order instead. Entries flow in a CSS multi-column box (top-to-bottom within a column, then across), not a row-major grid — the usual reading order for a printed or on-screen table of contents.

Search. The same sticky bar also gets a find-in-page text box — but only in the in-page overlay case (the same condition Printing above uses), sitting between the Contents toggle and the Top shortcut. A real popup window needs none: it's an actual OS window, so the browser's native Ctrl+F/Cmd+F already searches it — a custom box there would just be redundant chrome. The in-page fallback has no such native search reachable from inside the embedded WebView, which is the gap this fills. Matches are wrapped in plain <mark> elements — every browser already renders those as a highlighted span with no CSS needed, so there's no invented highlight colour to keep in sync with a host's theme; the current match additionally gets an outline so it stands out from the rest. Enter/Shift+Enter step through matches, Escape clears the query (or blurs if already empty), and the match count announces via aria-live for screen readers.

Matching wmap's reading measure. wmap's own guide content is capped at max-width: var(--wmap-guide-reading-width, 720px), widening to 1000px when the guide window is maximised (the property is set on the shared content wrapper, so it cascades to anything using it). Give the host content's own top-level block the same max-width: var(--wmap-guide-reading-width, <yourDefault>) — and matching padding, e.g. padding: 24px 32px — to keep the two sections' margins and line lengths visually identical as the window resizes, rather than introducing a visible seam between "the host's part" and "wmap's part."


A multi-map gallery with a shared control bar, per-card view-only toolbars, and click-to-detach into separate windows. All cards stay in sync — changing mode, colour, rotate, or flip in the gallery bar applies to every card instantly.

renderWaferGallery(container: HTMLElement, items: Array<WaferMapDisplayItem | WaferMapDisplayItemFactory>, options?: GalleryOptions): GalleryController

As with renderWaferMap, options is optional. GalleryOptions has 16 top-level fields; tsmap passes the same six it passes to renderWaferMap. The two option types deliberately overlap, so what you learned there mostly carries over — this section documents the gallery-only additions (columns, lotStatsSummary, per-card legends) and the shared fields' gallery behaviour.

The container needs a width but not a fixed height — the grid grows to fit its cards. width: 100% is the typical choice; do not set overflow: hidden on it or card content will be clipped.

<div id="gallery" style="width: 100%;"></div>

The gallery's toolbar and legend are position: sticky, so they stay visible while the grid scrolls. Stickiness needs a scrolling ancestor with a bounded height and overflow-y: auto (or scroll) — the container itself, or any parent. Without one, the toolbar and legend just scroll away with the grid as before; nothing breaks, they're simply not sticky.

import { renderWaferGallery } from '@wafertools/wafermap/render';

renderWaferGallery(document.getElementById('gallery'), items, galleryOptions);

6.1 WaferMapDisplayItem

A gallery item. WaferMapResult satisfies this interface structurally, so buildWaferMap results can be passed directly with no conversion. Only wafer and dies are required — everything else is optional, which allows synthesized items (e.g. stacked-mode aggregates) to be constructed without the full result shape.

interface WaferMapDisplayItem {
  wafer:         Wafer                                // required — wafer geometry
  dies:          Die[]                                // required — die data

  hbinDefs?:     BinDef[]                             // hard bin names/colors
  sbinDefs?:     BinDef[]                             // soft bin names/colors
  testDefs?:     TestDef[]                            // named test definitions
  metadataFields?: MetadataFieldDef[]                 // opts die.metadata keys into 'metadata' plot mode — §4.1.11
  reticles?:     Reticle[]                            // reticle field geometry

  label?:        string                               // names this wafer on its card header, in findings/yield lists, reports,
                                                      // the die list's Wafer column and a detached window — default: the
                                                      // wafer's metadata.waferId, else "Wafer N (no ID)"
  viewOptions?:  Partial<WaferViewOptions>            // per-card overrides merged on top of shared options
  statsSummary?:  StatsSummary                        // shown in the card's own summary panel when detached into its own window, and in the gallery Wafers panel; when lotStatsSummary is provided, per-wafer findings are available automatically — only set this explicitly when analysing without analyzeWaferLot
  onClick?:       (die: Die, event: MouseEvent) => void
  onSelect?:      (dies: Die[]) => void
}

Items are typically buildWaferMap results with display overrides spread in:

// Simplest form — result is a valid item as-is:
items = results.map(r => r);

// With display overrides:
items = results.map((r, i) => ({ ...r, label: ids[i] }));

// With per-card stats:
items = results.map((r, i) => ({ ...r, label: ids[i], statsSummary: summaries[i] }));

Reticle overlays are wired automatically from result.reticles — no extra configuration needed.

6.1.1 WaferMapDisplayItemFactory

type WaferMapDisplayItemFactory = () => WaferMapDisplayItem

A factory function accepted anywhere a WaferMapDisplayItem is expected (in the items array passed to renderWaferMap or setItems). When the gallery encounters a factory it inserts a placeholder card immediately and calls the factory in a deferred browser task (setTimeout(0)), swapping in the real card when it returns.

Use factories instead of pre-built items when buildWaferMap / analyzeWaferMap is expensive — the gallery shell and control bar appear instantly and cards fill in one by one rather than the page being blank while all maps are built:

const items = fixtures.map(sample => () => {
  const result  = buildWaferMap({ results: sample.results, passBins: [1] });
  const summary = analyzeWaferMap(result);
  return { ...result, label: sample.label, statsSummary: summary };
});

renderWaferGallery(container, items);

The placeholder card shows no label — if the label depends on computed data (e.g. a findings count) it appears when the card does. Pre-built items and factories can be mixed freely in the same array. Stacked modes (stackedValues, stackedBins, stackedSoftBins) require all items to be pre-built.

6.2 GalleryOptions

{
  viewOptions?:           WaferViewOptions  // initial shared state
  onViewOptionsChange?:   (opts: WaferViewOptions, changed: (keyof WaferViewOptions)[], category: 'preference' | 'state' | 'mixed') => void
                          // mirrors control bar changes; same category semantics as renderWaferMap
  downloadFilename?:       string             // stem for the composite PNG filename (default 'wafer-gallery')
  onSaveImage?:            (blob: Blob, suggestedName: string) => void | Promise<void>
                                            // host hook for persisting the composite gallery PNG (and each card's own
                                            // save). Mirrors renderWaferMap's onSaveImage — see §5.4 for full semantics.
  onSaveText?:             (text: string, suggestedName: string, mimeType: string) => void | Promise<void>
                                            // host hook for the Summary/Insights test-values table's "Export CSV" button.
                                            // Mirrors onSaveImage — see §5.4 for full semantics.
  showHelpButton?:         boolean           // show a help button in the gallery bar that opens the built-in end-user
                                            // guide (default false). Opens as a real, separate window when `window.open`
                                            // is available, falling back to an in-page non-modal floating window when
                                            // it's blocked (some embedded WebViews)
  userGuideExtension?:     UserGuideExtension  // insert a host app's own documentation into the guide window
                                            // (see "User guide extension" below) — only relevant when showHelpButton is true
  lotStatsSummary?:        LotStatsSummary   // lot-level stats from analyzeWaferLot — adds a Summary button to the toolbar; per-wafer findings are drawn from the lot analysis automatically and badged onto the Wafer Yield rows
  summaryPanel?:           SummaryPanelOptions  // Summary panel placement and open/closed initial state (§5.4.2)
  dieList?:                DieListDisplayOptions // "View die list" link inside the Summary panel — every die
                                            // across the whole lot, pooled, with a Wafer column. Not a toolbar
                                            // button; requires summaryPanel to be reachable. Default ENABLED
                                            // (shows automatically whenever the Summary panel is); set
                                            // { enabled: false } to hide it. §5.4.4
  insights?:               InsightsOptions   // adds an Insights toolbar button that swaps the grid for a lot-wide chart
                                            // suite (Overview, Distributions, Correlation, with a "Group by" control)
                                            // — default disabled. See §6.10.
  warnings?:               WarningsOptions   // built-in surfacing of the library's own advisories — ON by default.
                                            // Collected across every card and de-duplicated, so a problem affecting
                                            // the whole lot is stated once, not per wafer.
  columns?:                number            // fix the number of grid columns; omit to let the gallery auto-size based on die pitch
  zIndex?:                 number            // base z-index for wmap's transient overlays (menus, tooltip, modals); omit for a
                                            // safe high default, or set it to embed the gallery inside your own modal/overlay
                                            // (same semantics as renderWaferMap — see "Overlay z-index" in §5.4)
}

6.3 GalleryController

{
  setItems(items: Array<WaferMapDisplayItem | WaferMapDisplayItemFactory>): void  // rebuild all cards; factories resolved progressively
  setOptions(opts: Partial<WaferViewOptions>): void // sync shared options to all cards
  getOptions(): WaferViewOptions
  setLotStatsSummary(summary: LotStatsSummary | undefined): void  // update the lot summary panel at runtime
  openUserGuide(): void   // opens the end-user guide window directly — the same action the help toolbar button
                                  // performs, but callable regardless of showHelpButton, so a host that hides
                                  // wmap's own help button (e.g. folding it into its own combined help menu) can
                                  // still trigger the guide without a DOM query
  destroy(): void
}
Button Action
Mode Dropdown: plot mode for all cards
Palette Menu: bin colour scheme (bin modes) or value gradient (other modes) for all cards — same menu as §5.6. Bin colours are resolved once across the whole gallery and handed to every card, so a bin is the same colour on every wafer and in the lot legend. Hidden in 'metadata' mode — that mode always uses its own dedicated ordered palette (§4.1.11), so the control would have no visible effect.
Log scale Toggle log₁₀ scale for all cards. Shown only in value / stackedValues modes, and hidden whenever a solid pass/fail display is active or the active test is functional, since log scale has no effect on pass/fail colouring.
Rings Toggle ring boundaries on all cards
Quadrants Toggle quadrant boundaries on all cards
Labels Toggle die labels on all cards
Reticle Toggle reticle overlay on all cards — only shown when at least one item has reticle geometry
XY indicator Toggle axis-orientation arrows on all cards
Legend style Dropdown: Legend on each map (perCardLegend, off by default; forced on in value modes, where no lot-level colorbar exists), then Position on each mapDefault (right), Compact (right), Left, Top, Bottom, Floating (legendPosition). Positions are greyed while per-card legends are off, and hidden outside hardBin/softBin/metadata modes. The lot-level legend strip is not affected by either.
Orientation Dropdown: Rotate 90° CW, Flip horizontal, Flip vertical — applies to all cards
Columns Dropdown: fix the column count to 1–5, or restore Auto (default). Auto sizes columns so dies are at least 4 px wide. Cards are capped by die density and pack from the left rather than stretching to fill the width.
Download gallery Composite PNG of all cards at full HiDPI resolution
Summary Toggle the Summary panel — shown when lotStatsSummary is provided or any item carries statsSummary
Insights Toggle the Insights tab — swaps the grid for a lot-wide chart suite. Only shown when insights.enabled: true. See §6.10.
User guide Open the built-in end-user guide — a real, separate window when available, falling back to an in-page non-modal floating window when window.open is blocked (some embedded WebViews). Only shown when showHelpButton: true; callable directly via openUserGuide() regardless.

Per-card toolbars show only: box-select (when onSelect provided), zoom +/−, reset, download.

While the Insights tab is open, the grid/mode/palette/overlay/orientation/columns/download controls above, and Summary, are all hidden as a group — none of them apply to the chart suite, and Summary specifically toggles the gallery's Summary panel, which lives inside the grid body already hidden underneath. Summary, Insights, and User guide stay visible.

6.5 Summary panel

When lotStatsSummary is provided or any item carries statsSummary, a Summary panel toggle button appears in the control bar. Clicking it opens one panel (no tabs — see §5.5) alongside the grid. With lotStatsSummary it shows yield, bin breakdown and ring/quadrant yield across all the wafers, test value statistics, cross-wafer findings (repeated patterns, yield outliers), and a Wafer Yield list with each wafer badged by its own findings count; a "Summary report" button opens the full renderLotSummaryReportHtml document. Without lotStatsSummary, the panel lists the wafers that have findings of their own.

The header names the population: Summary — Lot LOT123 · 13 wafers when every wafer records the same lot ID, otherwise Summary — 26 wafers from 2 lots (or just 13 wafers when none records a lot). Statistics taken across the set follow the same rule — "lot median" only for one lot, "median of all wafers" otherwise — so a gallery pooling several lots never reads as one.

analyzeWaferLot runs per-wafer analysis internally, so passing lotStatsSummary alone populates the panel — no separate analyzeWaferMap per item is needed.

Clicking a finding highlights the affected area:

  • Repeated-pattern findings (e.g. ring or quadrant patterns seen across multiple wafers) — outlines the affected cards and highlights the matching die zone on each
  • Inter-wafer yield outliers — outlines the outlier card(s)

Clicking the active finding again clears the highlight. Detaching a card while a finding is active passes through the card's statsSummary so the window's own per-wafer summary panel is also available.

6.6 Detaching a card into its own window

Each card header contains an expand button (↗). Clicking it detaches that card into its own real, separate window (window.open) — not an in-page overlay — so it can be moved anywhere on screen, including outside the bounds of the host browser/app window, same as any other OS-managed window. The gallery grid stays fully interactive the whole time (there was never a backdrop or overlay to block it), and any number of cards may be detached at once, so several wafers can be inspected side by side. The detached window mounts a fresh renderWaferMap at full resolution with the complete toolbar; shared view options are passed through so it opens in the same display state as the gallery.

The card left behind in the grid becomes a small placeholder — its own controller is torn down while detached (the popup is the only live view of that wafer) — and its header button toggles to a "reattach" affordance (same button, no new UI). Clicking it, or closing the popup window itself, tears the popup down and rebuilds a fresh card in the grid slot with the gallery's current shared view options.

If the gallery's card set changes shape while a card is detached — most notably a stacked-mode switch, which can collapse many per-wafer cards into fewer aggregate ones — the popup has no equivalent grid slot to return to. Rather than closing or breaking, it becomes unlinked: its window title and an in-content banner both switch to an "— unlinked from gallery" notice, its own canvas/toolbar keep working exactly as before, and it can only be closed manually from then on (there is no longer a slot to reattach to).

Embedded hosts where window.open is blocked (e.g. Tauri, Electron, WebView2). Just like openHtmlReport (§7.9), a plain window.open call is blocked/returns null silently in these environments. Rather than leaving the detach button inert there, wmap automatically falls back to the same in-page non-modal floating window the user guide uses whenever window.open is unavailable and no custom opener is registered — detach keeps working everywhere, it just can't be dragged outside the host window's own bounds in that fallback case.

Non-modal floating windows can be minimized. Both the detach fallback window above and the user guide's own window (§6.5) show a minimize button in their header, alongside maximize/close. Minimizing collapses the window to a 220px-wide title strip — the map/content is hidden, not destroyed, and clicking again restores it to its previous size (including any size the user resized it to). The title truncates with an ellipsis and a native hover tooltip while minimized. Modal overlays (the single-map expand modal opened via the toolbar's expand button or E) do not get this button, since a modal's backdrop already blocks the rest of the page — minimizing one would achieve nothing.

If your host has its own multi-window API and you want a real separate OS window instead of the in-page fallback, register a custom opener at startup:

import { setDetachWindowOpener } from '@wafertools/wafermap/render';

setDetachWindowOpener((label) => {
  // Return a Window-like handle (must expose a usable `.document`, `.closed`,
  // and `.close()`) backed by your host's own window API. Return null to
  // decline — falls back to the in-page floating window, same as if no
  // opener were registered at all.
  return myApp.openDetachWindow(label);
});

Not usable for Tauri as designed. A Tauri WebviewWindow is fully isolated — a separate script context with its own window/document and no shared JS state with the window that created it — so it cannot produce the synchronous Window-with-live-.document handle this contract expects. Tauri hosts get the in-page fallback automatically with no configuration; a real Tauri-backed detach window would need a different mechanism entirely (a dedicated bootstrap page + IPC to pass the wafer data across, since there's no way to share a DOM reference between Tauri windows) — tracked as an open, unscoped item in tsmap's own issue log if you're building a Tauri host and want to pick this up.

6.7 Shared bin legend & lot metadata strip

A shared strip is rendered between the control bar and the card grid, combining two independent pieces of content:

  • Bin swatches — for hardBin and softBin modes only, one coloured swatch and label per unique bin across all items. Hidden for value, stackedValues, stackedBins, and stackedSoftBins (those modes use a per-card colorbar instead), unchanged from before.
  • Lot-level metadata (lot, product, test program, temperature, etc.) — shown in every mode, not just bin modes, so basic wafer/lot identity is never hidden behind a mode switch. Built on buildFacetTable (@wafertools/wafermap/stats): a field with a single value across every currently-shown item shows it plainly (Lot: LOT123); a field that varies shows every distinct value it takes (Lot: LOT123, LOT456) — never analyzeWaferLot's first-wafer-wins lotIdentity, and never silently dropped just because a gallery spans multiple lots. A field with many distinct values truncates to the top few (by wafer coverage) plus a +N more suffix, matching the die-hover tooltip's own +N more tests convention, so it never grows unbounded. waferId stays excluded from this strip by default (unique per wafer, never a useful summary value — the same curation buildFacetTable already applies for the Insights "Group by" control). In a stacked mode, also leads with "N wafers stacked · <method>".

The strip is hidden only when there is nothing to show at all — no metadata and no bin swatches for the current mode.

The richer per-wafer fields this strip's distinct-value list doesn't fully spell out are also available per card: each card's header is expandable — click it (or the chevron next to the label) to reveal that wafer's own full metadata as an overlay under the header, not a layout push, so it never resizes the card's map. Only rendered when the wafer actually has metadata to show. A card detached into its own window — a real popup, or the in-page floating-window fallback used when window.open is unavailable — carries the exact same expandable header, so identity and metadata read identically wherever a wafer from this gallery is being viewed.

The bin swatches use bin definitions from the gallery items — hbinDefs for hardBin mode, sbinDefs for softBin mode. Because hard and soft bin number spaces are independent (STDF V4: both 0–32767), the two arrays are kept separate and never merged.

Clicking a bin entry calls setOptions({ highlightBin: bin }), which dims all non-matching bins on every card simultaneously. Clicking the active entry clears the highlight. The active entry is indicated with a bold label and a blue swatch border. The strip rebuilds automatically whenever the mode, colour scheme, highlight, or item set changes.

6.8 Stacked modes

The toolbar includes three lot-aggregation modes: Stacked Hard Bins, Stacked Soft Bins, and Stacked Test Values. The gallery handles aggregation internally.

Bin and test definitions are read from the gallery items automatically — no need to pass them in viewOptions. The gallery discovers unique values from the input dies to generate the cards and legend.

  • stackedBins / stackedSoftBins — one card per bin; each die shows the count of wafers on which that bin appeared at that position.
  • stackedValues — one card per test parameter; each die shows the lot aggregate (mean by default) of that parameter. The aggregation method is sharedOpts.aggregationMethod (default 'mean'); change it with ctrl.setOptions({ aggregationMethod: 'median' }).

Switching to a stacked mode rebuilds the cards; switching back restores the original per-wafer cards. ctrl.setItems(newItems) always accepts per-wafer items — the gallery re-aggregates automatically if a stacked mode is active.

import { buildWaferMap } from '@wafertools/wafermap';
import { renderWaferMap } from '@wafertools/wafermap/render';

const results = waferIds.map(id => buildWaferMap({ results: dataByWafer[id], dieConfig }));
const items = results.map((r, i) => ({
  ...r,
  label:    waferIds[i],
  onClick:  (die) => showDieDetail(die, waferIds[i]),
  onSelect: (selected) => showSelectionPanel(waferIds[i], selected),
}));

const ctrl = renderWaferGallery(document.getElementById('gallery'), items, {
  viewOptions: { plotMode: 'hardBin' },
  onViewOptionsChange: (opts, changed, category) => syncSidebarControls(opts, changed, category),
  downloadFilename: 'lot-overview',
});

// Rebuild after wafer selection changes:
ctrl.setItems(newItems);

// Sync from external control:
ctrl.setOptions({ plotMode: 'value' });

// Clean up:
ctrl.destroy();

6.10 Insights tab

Passing insights: { enabled: true } adds an Insights toolbar button. Clicking it swaps the grid for a lot-wide chart suite, computed from every gallery item's dies — mutually exclusive with the grid view, since the chart suite wants the full body's room, not a side panel. The gallery grid's own state (mode, columns, etc.) is preserved underneath and restored when you switch back. Independent of the Summary panel (§6.5, opened separately) — the two toggle independently and neither hides the other's toolbar button, since the Summary panel's click-to-highlight has nothing to act on while Insights has replaced the grid.

Insights has the same three sub-tabs as the single-wafer version (§5.9) — Overview, Distributions, Correlation — plus:

  • Group by. When any gallery item's wafer.metadata has more than one distinct value for a groupable field (lot, product, testProgram, temperature, split, or any custom key), a "Group by" dropdown appears above the panels. waferId is deliberately never offered — every panel already shows one row/box per wafer when ungrouped, so "grouping" by wafer identity would just recreate that with an extra click. Each panel consumes the active grouping differently, matching what makes sense for that chart type:
  • Yield / bin pareto — one pooled bar per group, click-to-drill into that group's per-wafer bars with a Back button. Grouped bin pareto swaps to a clustered view (one bar per group, side by side, per bin) instead of drilling.
  • Boxplot — one pooled row per group by default, click-to-drill into that group's per-wafer rows with a Back button.
  • Histogram — an overlaid multi-series view, one coloured series per group, with a click-to-emphasize legend.
  • Capability / correlation matrix — a "Group: <value> ▾" dropdown that restricts to exactly one group's dies at a time (pooling across groups would be misleading for both — Simpson's paradox for correlation, mixed-population statistics for capability).
  • Scatter — never restricts; every group's points are always plotted together, coloured by group instead of hard bin, with a click-to-filter legend.
  • Wafer picker. Histogram, correlation, and scatter each pool every wafer by default when ungrouped (they draw one shared canvas, not one per wafer) — a "Wafer: All wafers ▾" selector lets you narrow to a single wafer instead. Narrowing also clears the "Mixed <fields> — Simpson's paradox" warning that correlation/scatter show when the pooled wafers vary on a groupable field, since a single wafer can't be mixed with anything.
  • Click to open a wafer. Leaf rows in the yield bar and the boxplot (a real per-wafer row — ungrouped, or drilled into a group) open that wafer in a modal, reusing the same detach-window rendering the gallery's card-expand feature uses. A boxplot leaf click opens the wafer already in test-value mode on the boxplot's currently selected test, not the default plot mode. Bin pareto/cluster bars and capability/correlation/scatter are not clickable-to-open.
  • Precomputed lot yield. When lotStatsSummary is also provided, the yield panel reads each wafer's yield directly from lotStatsSummary.lotYieldSeries instead of recomputing it from dies — guaranteeing the Insights tab's yield numbers agree exactly with the gallery's own Summary panel (§6.5) and any report generated from the same lotStatsSummary. Falls back to computing from dies (same passBins/exclusion rule as buildWaferMap) when lotStatsSummary is absent.
const items = results.map((r, i) => ({ ...r, label: waferIds[i] }));
const lotSummary = analyzeWaferLot(items.map(it => ({ dies: it.dies, wafer: it.wafer })));

renderWaferGallery(document.getElementById('gallery'), items, {
  insights: { enabled: true },
  lotStatsSummary: lotSummary,
});

analyzeWaferLot → §7.2 · LotStatsSummary → §7.5


7 Statistics / Findings Engine

The stats engine detects statistically significant spatial patterns in wafer test data — yield loss, bin accumulation, or test value shifts concentrated in rings, quadrants, angular sectors, reticle positions, contiguous failure clusters, and edge arcs. It is pure (no DOM) and can run in Node.js.

Use analyzeWaferMap for a single wafer. Use analyzeWaferLot when you have a full lot and want cross-wafer patterns and outlier detection on top.

import { analyzeWaferMap, analyzeWaferLot } from '@wafertools/wafermap/stats';

7.1 analyzeWaferMap(input, options?)

analyzeWaferMap(input: WaferMapResult | WaferMapInput, options?: AnalyzeWaferMapOptions): StatsSummary

WaferMapResult → §4.2 · WaferMapInput → §4.1 · AnalyzeWaferMapOptions → §7.3 · StatsSummary → §7.4

Analyses a single wafer and returns a StatsSummary. Accepts either a WaferMapInput object or the WaferMapResult returned by buildWaferMap — passing the result is preferred because passBins and testDefs are inferred automatically.

const result  = buildWaferMap({ results, waferConfig, dieConfig, passBins: [1] });
const summary = analyzeWaferMap(result);

// findings is pre-sorted: 'unusual' first, then 'notable', then 'info'
console.log(summary.findings[0]?.summary);
// e.g. "Ring 4 (edge) yield is 18.3 pp lower than the rest of the wafer"

// Pass to renderWaferMap to add a findings panel to the toolbar:
renderWaferMap(container, result, { statsSummary: summary });

Analysing a lot-stack result — pass a WaferMapResult built with lotStack directly. Ring, quadrant, sector, and reticle-position analysis run on the aggregated test values. When the active test has spec limits (limitLow / limitHigh in testDefs), out-of-spec dies are used as the failure proxy for cluster and edge-arc detection. If no spec limits are defined, cluster detection is skipped automatically.

const result = buildWaferMap({
  lotStack:    { results: waferResults, method: 'mean' },
  waferConfig, dieConfig, testDefs,
});
const summary = analyzeWaferMap(result);
renderWaferMap(container, result, { statsSummary: summary });
// summary.stats.isLotStack === true
// summary.stats.aggregationMethod === 'mean'

7.2 analyzeWaferLot(items, options?)

analyzeWaferLot(items: Array<WaferMapResult | WaferMapInput>, options?: AnalyzeWaferMapOptions): LotStatsSummary

WaferMapResult → §4.2 · WaferMapInput → §4.1 · AnalyzeWaferMapOptions → §7.3 · LotStatsSummary → §7.5

Analyses an array of wafers together and returns a LotStatsSummary. Use this when you have a full lot and want findings that span wafers — patterns on a single wafer are available in perWafer[i].summary, while lot-level findings cover the whole lot.

Each element is a WaferMapInput or WaferMapResult. In addition to per-wafer analysis, the lot summary adds:

  • Repeated-pattern findings — ring, quadrant, or sector patterns present on ≥ 2 wafers
  • Inter-wafer yield outliers — wafers whose yield is a statistical outlier within the lot
const waferResults = waferDataSets.map(d => buildWaferMap(d));
const lotSummary   = analyzeWaferLot(waferResults);

// Per-wafer findings:
console.log(lotSummary.perWafer[0].summary.findings);

// Lot-level findings (repeated patterns + outliers):
console.log(lotSummary.findings);

// Pass to renderWaferGallery to add a lot summary panel to the gallery bar:
renderWaferGallery(container, items, { lotStatsSummary: lotSummary });

7.3 AnalyzeWaferMapOptions

Both analyzeWaferMap and analyzeWaferLot accept these options, and most analyses work well with the defaults. Ring count is not one of them: it is set once on buildWaferMap (ringCount) and read from the result, so ring boundaries on the map and ring findings always agree.

{
  // ── Cost switches — every other analysis always runs ──────────────────────
  enableTestValueAnalysis?:       boolean  // default FALSE — expensive regional Welch pass on test values
                                           // (scales with regions × tests × dies). Opt in only when you
                                           // display the regional test-value findings. Implies perTestStats.
                                           // See the Performance guide for measured costs: performance.md
  computePerTestStats?:           boolean  // default false — cheap per-test quartile scan into perTestStats
                                           // (mean/stddev/median/q1/q3) WITHOUT the regional Welch pass.
                                           // Use this for box-plot / histogram panels. Implied by
                                           // enableTestValueAnalysis.

  // ── Test-value scope ──────────────────────────────────────────────────────
  testNumbers?:   number[]  // restrict test-value analysis to these test numbers;
                            // when omitted: all tests up to 250 — beyond that analysis is skipped
                            // and a 'test-count-capped' WaferWarning appears in
                            // summary.stats.warnings[] (§4.2.2) and in the map's
                            // warning indicator

  // ── Angular analysis ──────────────────────────────────────────────────────
  sectorCount?:             number  // sectors for angular analysis: 4 | 8 | 16 | 32 (default 8)
}

Removed in 0.27.0 — significanceLevel, minimumEffectSize, minimumRelativeEffect. These set what counts as a finding, so a wrong value did not make the output look different — it made it wrong, and silently. A negative significanceLevel returned zero findings across the board, which reads as "nothing wrong with this wafer": the worst failure an analysis tool has. They are internal constants now, like minimumSampleSize always was. Their values, and the gates they drive, are documented in §7.3.2 — that is what callers actually needed. Passing them from untyped JavaScript no longer takes effect; the value is validated, ignored, and reported via a WaferWarning.

Removed in 0.30.0 — the per-analysis switches (enableYieldAnalysis, enableHardBinAnalysis, enableSoftBinAnalysis, enableReticlePositionAnalysis, enableTestSiteAnalysis, enableClusterAnalysis, enableAngularAnalysis, enablePatternClassification). Every analysis now runs: yield, hard and soft bins, reticle positions (when reticleConfig is set), test sites (when the wafer has meaningful site duplication), angular sectors, clusters and pattern classification. Each was cheap and on by default. To show fewer findings, filter them with filterFindings (§7.14). Passing one from untyped JavaScript is ignored and reported as an analysis-option-corrected warning.

Every numeric option here is validated. A value outside the range that can produce a meaningful analysis is corrected to the nearest usable one and reported as an 'analysis-option-corrected' WaferWarning in summary.stats.warnings[] (§4.2.2) — visible in the renderers' warning indicator. sectorCount must be 4, 8, 16 or 32. ringCount is validated the same way but on buildWaferMap, where it is set, and reported in result.warnings; there is deliberately no upper bound on it, since a fine banding is still gated by the minimum region size.

7.3.1 Choosing what to analyse — cost, and who decides

Every analysis runs except two, which cost real time and are yours to decide:

Option Cost Decide it
computePerTestStats cheap — a quartile scan Once, for your whole app. On if you show distribution or box-plot charts.
enableTestValueAnalysis ~1–2µs per (wafer × die × test) Per lot, not once. Milliseconds on one wafer; seconds on a lot.

enableTestValueAnalysis is off by default because it is the only option whose cost changes kind with lot size — see Performance for measured figures. Being off by default is not a recommendation to leave it off: it produces the regional test-value findings, and an integrator who never enables it ships a Findings list that silently omits a whole category.

Estimate before you decide:

const estimateMs = waferCount * diesPerWafer * testCount * 1.5 / 1000;

Below roughly a second, just run it — the user will not notice it against the parse and render that just happened, and asking is pure friction. Above that, run the analysis without it and pass a FindingsNotice so the Findings panel states what is missing and what computing it would cost. That way the absence is visible where the findings are, rather than depending on the user discovering a control elsewhere.

Do not leave enableTestValueAnalysis on unconditionally for a lot viewer. A 25-wafer lot with a few hundred tests takes tens of seconds, with no progress indication, and reads as a hang.

7.3.2 Statistical rules & thresholds

A finding is emitted only when it clears two independent gates: it must be statistically significant (p-value ≤ 0.05 after multiple-comparison correction) and large enough to matter in practice (either an absolute 20 pp delta, or a doubling of the background failure rate). Severity is then assigned based on how extreme the finding is. You can usually ignore this section — it explains why a particular pattern did or didn't produce a finding.

Default thresholds:

Option Default Applies to
significanceLevel 0.05 adjusted p-value threshold after per-family BH correction
minimumEffectSize 0.20 absolute proportion delta for yield/bin findings
minimumRelativeEffect 1.0 relative effect \|delta / background\| for yield/bin/cluster findings
minimum region size auto auto-scaled to ~1% of wafer die count (min 5); not user-configurable

Effect size gate for proportion findings (yield, hard bin, soft bin, cluster, edge-arc):

A finding is kept when it passes the significance test AND satisfies at least one of: - absolute |delta| ≥ minimumEffectSize (0.20 by default), or - relative |delta / background| ≥ minimumRelativeEffect (1.0 by default)

The relative criterion catches meaningful signals on low-failure-rate wafers where the absolute delta is small but still represents a large deviation from background. For example, with a 3% background failure rate a 4 percentage-point increase is a 133% relative elevation — it clears minimumRelativeEffect even though 0.04 is well under the 0.20 absolute gate. Note the converse: a 2 percentage-point increase on the same background is only a 67% elevation, which clears neither gate and produces no finding.

Effect size gate for test-value findings:

Test-value findings use Cohen's d (pooled standard deviation), not a proportion delta. Only minimumEffectSize applies (|effectSize| ≥ 0.20); minimumRelativeEffect is not used for these findings.

Tests implemented:

  • Yield / bin proportions: two-proportion z-test (per-region vs. rest of wafer)
  • Test-value comparisons: Welch-style t (z-approx) with pooled SD → Cohen's d effect size
  • Contiguous cluster / edge-arc: one-sided binomial test (cluster failure rate vs. wafer-wide background)

Multiple comparisons: p-values are adjusted per variable-family using a Benjamini–Hochberg FDR procedure (grouping key: variable.kind + comparison.family). Only findings that pass both the adjusted p-value gate and the effect size gate are emitted.

Severity mapping (how the severity field is derived):

For proportion findings, severity uses whichever criterion — absolute or relative — is satisfied:

Severity p-value Absolute delta Relative delta
unusual ≤ 0.01 ≥ 0.30 ≥ 2.5× background
notable ≤ 0.05 ≥ 0.20 ≥ 1.5× background
info any other passing finding

For test-value findings (Cohen's d): unusual when d ≥ 0.5 at p ≤ 0.01; notable when d ≥ 0.15 at p ≤ 0.05.

Cluster and edge-arc severity also considers cluster size — a large contiguous cluster is visually dominant even when the rate contrast against an elevated background is modest. An additional size criterion applies on top of the rate/relative thresholds above:

Severity Cluster fraction of wafer
unusual ≥ 10 % of all eligible dies
notable ≥ 3 % of all eligible dies

Either the rate criterion or the size criterion can trigger the severity level; both require p ≤ 0.01 (unusual) or p ≤ 0.05 (notable).

Behavioural notes:

  • Reticle-position analysis is enabled by default but only runs when a reticleConfig is present in the view.
  • Test-value analysis is auto-skipped if the data contains more than 250 distinct tests unless testNumbers is provided. The result is no test findings at all, not a trimmed set — nothing throws, so an empty findings list is indistinguishable from "nothing to report" unless you check. A WaferWarning with code 'test-count-capped' appears in summary.stats.warnings[] (§4.2.2), is shown by the renderers' warning indicator, and is also logged via console.warn.

7.4 StatsSummary

{
  level: 'wafer'
  hasNotableFindings: boolean          // true when any finding is 'notable' or 'unusual'
  findings: StatsFinding[]             // sorted by severity: 'unusual' first, then 'notable', then 'info'
                                       // findings[0] is always the highest-severity finding; no manual sort needed
  wafer?: Record<string, unknown>      // identity fields from waferConfig.metadata (lot, wafer ID, test date, etc.)
  // Note: this `stats` block is analysis metadata; StatsFinding also has its own
  // nested `stats` object (pValue, sampleSizeLeft, etc.) — two distinct sub-objects.
  stats: {
    totalDies:            number        // all dies on the wafer including partial and edge-excluded
    analyzedDies:         number        // dies included in analysis (excludes partial and, by default, edge dies)
    excludedDies:         number        // edge-excluded dies (see edgeDieYieldMode)
    yieldPercent:         number | null // (passDies / analyzedDies) × 100 ∈ [0, 100]
                                        // null when no die in the wafer has an hbin value at all
    testsConsidered:      number[]     // test numbers (keys from testValues) that had enough data
    hardBinsConsidered:   number[]
    softBinsConsidered:   number[]
    hardBinCounts?:  Record<number, number>  // die count per hard bin, over the yield-eligible population
                                              // (excludes partial/edge-excluded dies) — unlike hardBinsConsidered
                                              // above (which bin codes appear at all), these are the actual
                                              // counts a bin-breakdown display should show
    softBinCounts?:  Record<number, number>  // same, for soft bins
    warnings?:            WaferWarning[]  // structured advisories — §4.2.2. Same shape as
                                          // WaferMapResult.warnings, so a host has ONE warning
                                          // vocabulary. Branch on warning.code, e.g.
                                          // 'test-count-capped'. Was string[] before 0.22.0.
    isLotStack?:          boolean      // true when this summary was produced from lot-aggregated (lotStack) data
    aggregationMethod?:   string       // aggregation method used, e.g. 'mean', 'countBin' (present only when isLotStack is true)
    testSpecYield?: Array<{            // one entry per testDef that has at least one limit; absent when no testDefs with limits
      testNumber:   number
      label:        string            // testDef.name
      passDies:     number            // dies with value within [limitLow, limitHigh]
      failLowDies:  number            // dies with value < limitLow (0 when limitLow absent)
      failHighDies: number            // dies with value > limitHigh (0 when limitHigh absent)
      totalDies:    number            // dies that had a value for this test
      yieldPercent: number | null     // (passDies / totalDies) × 100 ∈ [0, 100]; null when totalDies = 0
    }>
    functionalYield?: Array<{         // one entry per functional (testType 'F') test — "functional yield" in fab terms;
                                      // verdicts read via getTestPassStatus (recorded testPass first, then the
                                      // legacy 0/1 testValues fallback); partial/edge-excluded dies excluded
      testNumber:      number
      label:           string         // testDef.name
      passDies:        number
      failDies:        number
      totalDies:       number         // dies with a recorded verdict — never counts untested dies as fails
      passRatePercent: number | null  // (passDies / totalDies) × 100 ∈ [0, 100]; null when totalDies = 0
    }>
    perTestStats?: Array<{            // present only when computePerTestStats or enableTestValueAnalysis is set;
                                      // one entry per active test with enough data
      testNumber: number
      label:      string             // testDef.name, or "Test {N}" when no testDef
      count:      number             // number of dies with a value for this test
      min:        number
      max:        number
      mean:       number
      stddev:     number             // sample standard deviation
      median:     number             // 50th percentile (linear interpolation)
      q1:         number             // 25th percentile
      q3:         number             // 75th percentile
    }>
  }
}

7.5 LotStatsSummary

{
  level: 'lot'
  hasNotableFindings: boolean
  findings: StatsFinding[]             // lot-level findings (repeated patterns + inter-wafer outliers); sorted unusual → notable → info
  lot?: Record<string, unknown>        // identity fields EVERY wafer with identity data agrees on (lot ID, product, etc. —
                                        // wafer-specific keys excluded). A key where wafers disagree (e.g. items pooled
                                        // from more than one lot/program) is omitted here, not silently taken from the
                                        // first wafer — see mixedIdentityFields below.
  mixedIdentityFields?: string[]       // identity keys where the pooled wafers do NOT all agree. Present only when at
                                        // least one such key exists — check this before treating `lot` as describing
                                        // the whole batch.
  stats: {
    waferCount: number
  }
  lotYieldSeries: Array<{
    waferIndex:   number
    yieldPercent: number | null        // (passDies / totalDies) × 100 ∈ [0, 100]; null when a wafer had no bin data
  }>
  perWafer: Array<{
    waferIndex: number
    summary: StatsSummary              // per-wafer findings
  }>
  perWaferTestStats?: Array<{          // present only when computePerTestStats or enableTestValueAnalysis is set
                                       // (prefer computePerTestStats — it skips the regional Welch pass)
    waferIndex: number
    tests: Array<{
      testNumber: number
      label:      string
      count:      number
      min:        number
      max:        number
      mean:       number
      stddev:     number
      median:     number
      q1:         number
      q3:         number
    }>
  }>
}

7.6 renderFindingsReportHtml

Deprecated — removed in 0.31.0, with §7.7, §7.8, openHtmlReport (§7.9) and openReportModal (§10.3). Reports open from the Summary panel's report button; setReportOpener (§7.9) routes them into your host and stays.

import { renderFindingsReportHtml } from '@wafertools/wafermap/stats';

renderFindingsReportHtml(summary: StatsSummary | LotStatsSummary, options?: { title?: string }): string

Generates a standalone printable HTML findings-only report from a StatsSummary or LotStatsSummary. Includes wafer/lot identity fields, yield and die count stats, and a severity-coded findings table. Open the result in a new tab with window.open('', '_blank') for printing or saving as PDF.

Findings that another finding absorbs as an exact restatement (§7.10, absorbedIds) are omitted, matching the Summary panel — the surviving row's text names what it absorbed, so printing both would state the same fact twice.

StatsSummary → §7.4 · LotStatsSummary → §7.5

7.7 renderSummaryReportHtml

Deprecated — removed in 0.31.0. See §7.6.

import { renderSummaryReportHtml } from '@wafertools/wafermap/stats';

renderSummaryReportHtml(params: SummaryReportParams, options?: { title?: string }): string

Generates a standalone printable HTML full summary report — a snapshot of everything shown in the summary panel: metadata, yield, bin breakdown, ring yield, quadrant yield, test value statistics (min/mean/median/stddev/max per test, labelled by testDef.name or Test {N} when testDefs is absent), a Process Capability section (Cp/Cpk/Pp/Ppk for every test in testDefs with both a lower and upper spec limit — omitted entirely when none qualify, same "appears automatically" convention as every other section), and findings. Open the result in a new tab with window.open('', '_blank') for printing or saving as PDF.

// SummaryReportParams
{
  wafer:         Wafer
  dies:          Die[]
  yieldSummary:  YieldSummary
  dataCoverage:  { filledDies: number; totalDies: number; edgeExcludedDies: number; ratio: number }
  hbinDefs?:     BinDef[]
  sbinDefs?:     BinDef[]
  testDefs?:     TestDef[]
  statsSummary?: StatsSummary
  passBins?:     number[]   // default [1]
  ringCount?:    number     // default 4
}

Wafer → §12.2 · Die → §12.1 · YieldSummary → §4.2.1 · BinDef → §4.1.9 · TestDef → §4.1.8 · StatsSummary → §7.4

The summary panel's "Summary report" button calls this automatically when statsSummary is provided, and opens the result via openReportModal (§10.3).

7.8 renderLotSummaryReportHtml

Deprecated — removed in 0.31.0. See §7.6.

import { renderLotSummaryReportHtml } from '@wafertools/wafermap/stats';

renderLotSummaryReportHtml(params: LotSummaryReportParams, options?: { title?: string }): string

Generates a standalone printable HTML full lot summary report — the lot-level equivalent of renderSummaryReportHtml. Covers lot overview stats, per-wafer yield table, bin breakdown, ring and quadrant yield, test value statistics across the lot, a Process Capability section (same rule as §7.7), a Splits section (one row per item with a wafer.metadata.split assigned — omitted when none do), and findings. Open the result in a new tab with window.open('', '_blank') for printing or saving as PDF.

// LotSummaryReportParams
{
  items: Array<{
    label:          string
    wafer?:         Wafer
    dies?:          Die[]
    passBins?:      number[]       // this wafer's own pass bins (WaferMapResult.passBins) — wins over the top-level fallback
    statsSummary?:  StatsSummary   // reused directly as analyzeWaferLot's perWaferSummaries — avoids re-running analyzeWaferMap
  }>
  hbinDefs?:       BinDef[]
  sbinDefs?:       BinDef[]
  testDefs?:       TestDef[]
  passBins?:       number[]   // fallback for items without their own passBins — default [1]
  ringCount?:      number     // default 4
  analyzeOptions?: AnalyzeWaferMapOptions  // passthrough to the internal per-group analyzeWaferLot call
}

Wafer → §12.2 · Die → §12.1 · BinDef → §4.1.9 · TestDef → §4.1.8 · StatsSummary → §7.4 · AnalyzeWaferMapOptions → §7.3

There is no lotSummary parameter — grouping, per-group analysis (analyzeWaferLot), and rendering all happen internally from the flat items list, so callers never pre-compute a lot summary or pre-partition by lot identity themselves. items is partitioned by whichever of lot/product/testProgram/temperature actually vary across the wafers' wafer.metadata (a split difference alone never triggers a split — comparing splits within one report is the point of that field, not a reason to separate them into different documents). The common single-lot case produces one report identical to a plain single-lot call; a load that spans more than one lot/product/program/temperature is split into multiple side-by-side sections instead of silently pooling stats across populations that shouldn't be averaged together, with a banner explaining the split.

The lot summary panel's "Summary report" button calls this automatically when lotStatsSummary is provided to renderWaferGallery, and opens the result via openReportModal (§10.3) (the on-screen panel and the generated report can legitimately show different numbers for a heterogeneous multi-lot load — the panel displays the host's own precomputed lotStatsSummary as a single pooled view, while the report always applies the identity-based split described above).

7.9 openHtmlReport / setReportOpener

openHtmlReport is deprecated — removed in 0.31.0. setReportOpener stays: it routes every report the Summary panel opens.

import { openHtmlReport, setReportOpener } from '@wafertools/wafermap/stats';

openHtmlReport(html: string): void
setReportOpener(opener: (html: string) => void): void

openHtmlReport opens a rendered HTML report string (from renderFindingsReportHtml, renderSummaryReportHtml, or renderLotSummaryReportHtml) in a new browser tab.

The summary panel's "Summary report" button no longer calls this directly — it opens through openReportModal (§10.3) instead, an in-app modal that needs no host wiring at all, in a plain browser tab or an embedded host (Tauri, Electron, WebView2) alike. openHtmlReport/setReportOpener are still very much alive: openReportModal's own "Open as full page ↗" link and print button route through them, so a host that wants the report as a real separate page (to keep it open outside the modal, or as a platform-specific print fallback) still registers an opener exactly as below — it's just no longer required merely to view a report. renderFindingsReportHtml remains available for a caller that wants a findings-only document of its own, opened however it likes (openHtmlReport, openReportModal, or its own presentation).

In embedded hosts where window.open is blocked (e.g. Tauri, Electron, WebView2), register a custom opener at startup if you want the "Open as full page" fallback to produce a real OS window/page instead of a console warning:

setReportOpener(html => {
  // e.g. write to a host-managed window, invoke an IPC call, etc.
  myApp.showReport(html);
});

Once set, openHtmlReport routes through your opener instead of window.open.

7.10 StatsFinding

{
  id:       string          // stable identifier for this finding
  level:    'wafer' | 'lot' | 'inter-wafer'
  severity: 'unusual' | 'notable' | 'info'
            // ranking (highest → lowest): unusual > notable > info
  variable: {
    kind:   'yield' | 'hardBin' | 'softBin' | 'test' | 'functionalTest' | 'spatialPattern'
    index?: number          // test number — the key from testValues (for 'test' kind)
    bin?:   number          // bin value (for 'hardBin'/'softBin' kind)
    label:  string          // human-readable name
    unit?:  string
  }
  comparison: {
    family: 'ring' | 'quadrant' | 'reticle-position' | 'test-site' | 'wafer'
          | 'sector' | 'cluster' | 'edge-arc' | 'spatial-pattern'
    left:   string          // e.g. "Ring 3 (edge)", "NE", "Rings 1–3", "Reticle cell (1, 0)"
                            // adjacent same-signal regions are merged into one finding (e.g. "Rings 1–3")
    right:  string          // typically "Rest of wafer", or for a yield outlier "Lot median"
                            // (every wafer records one lot) / "Median of all wafers" (otherwise)
  }
  effect: {
    direction:      'higher' | 'lower' | 'different'
    absoluteDelta?: number
    relativeDelta?: number
    effectSize?:    number
  }
  stats: {                   // per-finding test statistics (distinct from StatsSummary.stats)
    method:            string
    pValue?:           number
    adjustedPValue?:   number
    sampleSizeLeft:    number   // dies in the region (left side of comparison)
    sampleSizeRight:   number   // dies in the rest of the wafer (right side)
  }
  summary:   string         // one-sentence human-readable description — a plain string, not an object
                            // e.g. "Ring 4 (edge) yield is 18.3 pp lower than the rest of the wafer"
  highlight: HighlightTarget
  relatedIds?: string[]     // ids of findings this one summarises at a finer level:
                            // a spatial-pattern's supporting regional findings, or the per-region
                            // findings collapsed into a merged band ("Rings 1–3"). Audit/drill-down.
                            // NOTE: these do not all resolve to entries in `findings` — a merged
                            // band names the constituents it REPLACED, and those are gone.
  absorbedIds?: string[]    // ids of findings that state exactly the same fact as this one, hidden
                            // from the Summary panel and the findings report in favour of it.
                            // Unlike relatedIds, everything here IS still in `findings`.
                            // Two cases: a soft-bin finding whose hard-bin twin covers provably the
                            // same dies (merged label reads "Hard and soft bin 3 (same dies)"), and
                            // the single pass bin's row against the yield row restating it.
                            // Merging is on die-set identity, never bin number — hard and soft bins
                            // are independent number spaces.
}

7.11 HighlightTarget

Describes what to visually emphasise when a finding is selected.

type HighlightTarget =
  | { kind: 'region';  regionFamily: 'ring' | 'quadrant' | 'reticle-position' | 'test-site' | 'sector';
                        regionKeys: string[]; dieKeys?: string[] }
  | { kind: 'bin';     bin: number; regionKeys?: string[]; dieKeys?: string[] }
  | { kind: 'wafer';   waferIndices: number[] }
  | { kind: 'dies';    dieKeys: string[] }

dieKeys entries use the "x,y" format returned by getDieKey.

Highlight kind by finding family:

comparison.family highlight.kind Notes
ring region regionFamily: 'ring'
quadrant region regionFamily: 'quadrant'
reticle-position region regionFamily: 'reticle-position'
sector region regionFamily: 'sector'
cluster dies exact failing die keys
edge-arc dies exact failing die keys
wafer wafer lot-level only

7.12 Integrating with renderWaferMap

import { buildWaferMap } from '@wafertools/wafermap';
import { renderWaferMap } from '@wafertools/wafermap/render';
import { analyzeWaferMap, analyzeWaferLot } from '@wafertools/wafermap/stats';

// Single wafer with summary panel toggle:
const result  = buildWaferMap({ results, waferConfig, dieConfig, passBins: [1] });
const summary = analyzeWaferMap(result);
renderWaferMap(container, result, { statsSummary: summary });

// Lot gallery with lot-level summary panel toggle:
const waferResults = waferDataSets.map(d => buildWaferMap(d));
const items = waferResults.map((r, i) => ({
  ...r,
  label:        `Wafer ${i + 1}`,
  statsSummary: analyzeWaferMap(r),
}));
const lotSummary = analyzeWaferLot(waferResults);
renderWaferGallery(container, items, { lotStatsSummary: lotSummary });

7.13 Region builder utilities

Deprecated — removed in 0.31.0. These are the analysis's own region builders, exported by accident; analyzeWaferMap applies them and each finding names its region.

These are exported from @wafertools/wafermap/stats for use in custom analysis pipelines. They are also called internally by analyzeWaferMap.

import {
  buildRingRegions,
  buildQuadrantRegions,
  buildReticlePositionRegions,
  buildSectorRegions,
  buildTestSiteRegions,
} from '@wafertools/wafermap/stats';

buildRingRegions(dies: Die[], wafer: Wafer, ringCount: number): StatsRegion[]
// family: 'ring'; keys 'ring:1' ... 'ring:N'

buildQuadrantRegions(dies: Die[], wafer: Wafer, ringCount: number): StatsRegion[]
// family: 'quadrant'; keys 'quadrant:NE' etc.

buildReticlePositionRegions(dies: Die[], reticleConfig: ReticleConfig | undefined): StatsRegion[]
// family: 'reticle-position'; keys 'reticle-position:cell:C,R'
// returns [] when reticleConfig is undefined

buildSectorRegions(dies: Die[], wafer: Wafer, sectorCount: number): StatsRegion[]
// family: 'sector'; keys 'sector:N', 'sector:NNE', etc.
// sectorCount: 4 | 8 | 16 | 32 (default 16 if invalid value passed)
// dies with normalised radius < 0.2 are excluded (too close to centre)

buildTestSiteRegions(dies: Die[], forceEnable?: boolean): StatsRegion[]
// family: 'test-site'; keys 'test-site:N' (one per distinct siteNum value)
// groups dies by die.siteNum — no geometry required
// auto-enable guard: returns [] unless ≥2 distinct siteNum values each appear on ≥3 dies
//   (prevents spurious regions when siteNum is used as a monotonic counter rather
//    than a true parallel-site identifier)
// pass forceEnable=true to bypass the guard when you have already validated the data

Die → §12.1 · Wafer → §12.2 · ReticleConfig → §4.1.4

Each StatsRegion has:

{
  family:   'ring' | 'quadrant' | 'reticle-position' | 'test-site' | 'sector'
  key:      string   // unique region identifier
  label:    string   // human-readable (e.g. "Ring 4 (edge)", "Sector NNW", "Site 2")
  dieKeys:  string[] // "x,y" keys of dies in this region
}

Three region-ordering helpers are also exported from @wafertools/wafermap/stats. They are the single source of truth for sector and quadrant ordering, used by buildSectorRegions and by the internal adjacent-finding merge:

sectorCompassNames(sectorCount: number): string[]
// CCW-from-East compass names for 4 | 8 | 16 | 32 (defaults to the 16-point set otherwise)

areQuadrantsAdjacent(a: string, b: string): boolean
// edge-sharing quadrants (NE–NW, NE–SE, NW–SW, SE–SW) are adjacent; diagonals are not

parseRegionKey(key: string): { family, ring?, quadrant?, sector? }
// parse a region key ('ring:2', 'quadrant:NE', 'sector:NNE') into its structured parts

7.14 filterFindings(source, filter)

filterFindings(source: StatsSummary | LotStatsSummary, filter: FindingsFilter): StatsFinding[]

Filters findings from a StatsSummary or LotStatsSummary by any combination of severity, kind, family, and level. All criteria are ANDed; each accepts a single value or an array.

Operates on the complete findings list and deliberately does not apply the display de-duplication: a filter on kind: 'softBin' returns every soft-bin finding, including ones a hard-bin twin absorbed for display. If you are building a list for a human to read rather than querying, exclude absorbedIds as well — see §7.10 and the Developer Guide.

StatsSummary → §7.4 · LotStatsSummary → §7.5 · StatsFinding → §7.10

import { filterFindings } from '@wafertools/wafermap/stats';

// Unusual ring or quadrant findings only:
const critical = filterFindings(summary, {
  severity: 'unusual',
  family: ['ring', 'quadrant'],
});

// All yield findings across the lot:
const yieldFindings = filterFindings(lotSummary, { kind: 'yield' });
interface FindingsFilter {
  severity?: StatsSeverity | StatsSeverity[]
  kind?:     StatsVariableKind | StatsVariableKind[]
  family?:   StatsComparisonFamily | StatsComparisonFamily[]
  level?:    StatsLevel | StatsLevel[]
}

visibleFindings(findings)

visibleFindings<T extends { id: string; absorbedIds?: string[] }>(findings: T[]): T[]

Drops findings that another finding has claimed as an exact restatement of itself — a soft-bin twin covering the same dies, or the single pass bin's row against the yield row that says the same thing. The claimer's own label already names what it absorbed ("hard bin and soft bin 3 (same dies)"), so listing both prints one fact twice, once merged and once not.

Apply it to anything that renders a findings list. StatsSummary.findings deliberately keeps the full uncollapsed set, so a host that wants every row can still have it; this is the filter the built-in surfaces put in front of it.

import { visibleFindings } from '@wafertools/wafermap/stats';

for (const f of visibleFindings(summary.findings)) { /* … */ }

Composes with filterFindings in either order — the two are independent, one dropping absorbed restatements and the other narrowing by severity/kind/family/level.

7.15 classifyPattern(dies, wafer, options?)

Deprecated — removed in 0.31.0. analyzeWaferMap reports the classified pattern as a finding (comparison.family === 'spatial-pattern').

classifyPattern(
  dies:    Die[],
  wafer:   Wafer,
  options: {
    passBins:   number[],
    ringCount?: number,
  }
): PatternClassification | null

Classifies the spatial failure pattern of a wafer from its die data. Returns null when the number of failing dies is below the minimum threshold (auto-scaled, min 5).

Die → §2.3 · Wafer → §2.1 · PatternClassification → below

Called automatically by analyzeWaferMap. Call directly when you need the geometry features without the full analysis pipeline, or to use the raw PatternFeatures as input to your own classifier.

import { classifyPattern } from '@wafertools/wafermap/stats';

const result = buildWaferMap({ results, waferConfig, dieConfig, passBins: [1] });
const c = classifyPattern(result.dies, result.wafer, {
  passBins:  result.passBins,    // what the map was built with — never restate them
  ringCount: result.ringCount,
});

if (c) {
  console.log(c.pattern);     // 'edge-ring' | 'center' | 'scratch' | ...
  console.log(c.confidence);  // 'high' | 'medium' | 'low'
  console.log(c.note);        // advisory string when classification may be imprecise
  console.log(c.features);    // raw geometry numbers — use as ML input if needed
}

PatternClassification

interface PatternClassification {
  pattern:    PatternLabel           // detected pattern
  confidence: 'high' | 'medium' | 'low'
  features:   PatternFeatures        // raw geometry numbers
  note?:      string                 // advisory when classification may be imprecise
}

type PatternLabel =
  | 'center' | 'donut' | 'edge-ring' | 'edge-local'
  | 'scratch' | 'near-full' | 'random' | 'none'

PatternFeatures

The geometry numbers computed for every wafer — usable as input to a custom classifier:

interface PatternFeatures {
  globalRdd:         number  // failing / total eligible dies
  edgeRdd:           number  // failing in outermost ring / total outermost-ring dies
  centroidDistNorm:  number  // distance from wafer centre to salient-region centroid, / radius
  minDistNorm:       number  // min radial distance of salient-region dies, / radius
  maxDistNorm:       number  // max radial distance of salient-region dies, / radius
  p25DistNorm:       number  // 25th-percentile radial distance of all failing dies, / radius
  eccentricity:      number  // 0 = circle, 1 = line (from covariance of top-5 components)
  linearScore:       number  // fraction of top-5 component dies on the best row/col/diagonal
  salienceSize:      number  // die count of the largest connected component
  salienceFraction:  number  // salienceSize / total failing dies
  edgeAngularSpread: number  // fraction of 16 circumference sectors covered by edge-zone fails
  innerOuterRatio:   number  // fail rate inner half / fail rate outer half
}

See Pattern Detection for benchmark accuracy figures and known limitations.

7.16 Chart-data builders

Pure, DOM-free data builders for the chart types the Insights tab (§5.9, §6.10) draws internally. Each takes plain { dies?: Die[] }-shaped items (or a wafer.metadata-carrying superset for the faceting ones) and returns plain data — no canvas, no rendering. The chart-data builders are deprecated and will be removed in 0.31.0. They were public so a host could draw these charts itself; the Insights tab (insights: { enabled: true }) now draws them. Each still works and logs one console notice on first use. computeFunctionalYield (read stats.functionalYield) and DEFAULT_FACET_CURATION (which buildFacetTable applies by default) are deprecated too; buildFacetTable, facetValueOf, FACET_NONE_VALUE and mergeTestDefs are not. If you depend on a builder, say so at https://github.com/wafertools/wafermap/issues.

All die-population rules match wmap's own conventions elsewhere: yield/bin builders exclude partial/edgeExcluded dies via the same isYieldEligibleDie rule buildWaferMap/analyzeWaferMap use (§11.20); a die with no hbin/sbin is never coerced into a real bin (bin 0 is reserved as the "no data" category everywhere in wmap, matching every registered colour scheme's palette).

import {
  buildFacetTable, facetValueOf, FACET_NONE_VALUE,
  mergeTestDefs,
} from '@wafertools/wafermap/stats';

// Deprecated — each logs one notice on first use:
import {
  buildYieldData, buildYieldDataCombined,
  buildBinParetoData, buildBinClusterData,
  buildCapabilityData,
  buildTestBoxplotData,
  buildTestTrendData, trendCentre,
  buildTestPassRateData, hasJudgeableTests,
  buildTestHistogramData, buildTestHistogramSeries,
  buildCorrelationMatrix, filterCorrelationMatrix,
  buildScatterData, buildScatterDataGrouped,
  computeFunctionalYield, DEFAULT_FACET_CURATION,
} from '@wafertools/wafermap/stats';
Function Returns Notes
buildYieldData(items, passBins?, sortBy?) ChartDatum[] Deprecated. One row per item. Prefers each item's precomputed yieldPercent (e.g. from LotStatsSummary.lotYieldSeries) over recomputing from dies, so it agrees byte-for-byte with whatever else already reports that wafer's yield. sortBy: 'yield' \| 'label' (default 'label'). ChartDatum.key, when the input item carried one (YieldItem.key), is carried through unchanged — resolve a clicked row back to your own item by key, not label (two items can share a label, e.g. if neither supplies one).
buildYieldDataCombined(groups, passBins?, sortBy?) ChartDatum[] Deprecated. One row per group — the mean of the group's per-item yields, weighted by each item's yield-eligible die count (excludes partial/edgeExcluded dies, matching what the yield rate itself was computed over) — not raw dies.length, which would let dies that never counted toward an item's own yield still skew the combined average.
buildBinParetoData(items, binType) ChartDatum[] Deprecated. One row per bin (binType: 'hbin' \| 'sbin'), sorted by count descending. An item carrying hardBinCounts/softBinCounts (e.g. StatsSummary.stats.hardBinCounts) contributes those directly instead of re-walking its dies.
buildBinClusterData(groups, binType) BinClusterData Deprecated. Every group's bin counts side by side — { groups: string[], bins: BinCluster[] }, one BinCluster per bin with a counts[] aligned to groups.
buildCapabilityData(items, testDefs) CapabilityDatum[] Deprecated. Cp/Cpk (pooled within-item stddev — each item is treated as the short-term subgroup) and Pp/Ppk (overall stddev), for every parametric test with at least one recorded value. Tests with both limitLow and limitHigh get hasSpec: true, full capability indices, and min/q1/median/q3/max normalized (v - lsl) / (usl - lsl). Tests missing one or both limits still appear (hasSpec: false, lsl/usl/cp/cpk/pp/ppk all absent/null) normalized onto their own observed [min, max] instead — a lot with sparse spec coverage no longer renders empty. Sorted spec'd-first (worst-Ppk-first within that tier), then unspec'd (most-variable-first).
computeFunctionalYield(dies, testDefs) StatsSummary['stats']['functionalYield'] Deprecated. Per-test pass rate for every functional (testType: 'F') test — pass/fail/verdict counts and passRatePercent. Verdicts read via getTestPassStatus (recorded testPass first, then the legacy 0/1 fallback); partial/edge-excluded dies excluded; dies with no verdict are never counted as fails. The same computation analyzeWaferMap uses for stats.functionalYield and the "Functional Tests" tables.
buildTestBoxplotData(items, testNumber) BoxplotDatum[] Deprecated. One five-number summary (min/q1/median/q3/max/count) per item, for one test. Excludes partial/edge-excluded dies. An item carrying testStats with an entry for the requested test (e.g. from StatsSummary.stats.perTestStats) uses it directly instead of re-scanning dies.
buildTestTrendData(items, testNumber) TrendDatum[] Deprecated. One point per item — mean, sample stddev (ddof=1), count — for one test, in the order given and never sorted. Slot order is the entire signal: a drift or a bad cassette position only reads on the population's own sequence, so sorting by value would destroy what the chart exists to show. Items with no values for the test are kept in place with count: 0 and mean: NaN rather than dropped, so a gap in the sequence stays visible. Like buildTestBoxplotData, an item carrying testStats for the requested test (e.g. StatsSummary.stats.perTestStats) is used directly instead of re-scanning dies. TrendDatum.key carries the caller's own identity through for click-to-open.
trendCentre(data) number \| null Deprecated. The population mean across every TrendDatum that has data, n-weighted — the mean of the pooled dies, not a mean of per-wafer means, so it does not let a short wafer count as much as a full one. null when nothing has data. This is the trend chart's dashed centre line.
buildTestPassRateData(groups, testDefs, kind) TestPassRateData Deprecated. Per-test pass rate, one row per test (worst first) with a value per group. kind (TestPassKind) selects which pass/fail notion is measured, and the three are not interchangeable: 'spec' judges a parametric value against its limitLow/limitHigh; 'testFlag' reads the tester's own recorded verdict (die.testPassSTDF's PTR TEST_FLG, which exists whether or not limits do); 'functional' covers pass/fail-only (testType: 'F') tests. The two parametric modes can legitimately disagree (guard bands, dynamic or per-site limits, a limits/data mismatch), so TestPassRateData.disagreementDies counts the dies judged differently rather than resolving them — null, distinct from 0, when only one source exists. Rates only, on a fixed 0–100% axis: groups routinely differ in wafer count, and a count axis would show the larger group failing more while failing at the same rate. A parametric test with neither limits nor a recorded verdict is unjudgeable and is omitted rather than reported as 100%; a group that never ran a test yields no value rather than a 0% one.
hasJudgeableTests(groups, testDefs, kind) boolean Deprecated. Whether kind would produce anything — use it to offer only the modes the data supports. For 'testFlag' it takes the dies, not just the definitions: every parametric test could carry a verdict, so a definition-only check would offer a mode that renders empty.
buildTestHistogramData(items, testNumber, bucketCount?, limitLow?, limitHigh?) HistogramBucket[] Deprecated. Bucketed value counts across items, pooled.
buildTestHistogramSeries(groups, testNumber, bucketCount?, limitLow?, limitHigh?) HistogramSeriesData Deprecated. Shared bucket ranges with one count series per group — { ranges, series: [{ groupKey, counts }] }.
buildCorrelationMatrix(dies, testDefs) CorrelationMatrix Deprecated. Pearson r for every parametric test pair.
filterCorrelationMatrix(matrix, options) { matrix, strongPairs, moderatePairs, hiddenWeakPairs, strongestPair } Deprecated. Caps matrix size (options.maxTests) and requires a minimum test count (options.minTests), keeping the pairs with the largest correlation magnitude.
buildScatterData(items, xTest, yTest) ScatterPoint[] Deprecated. One point per die with valid values for both tests.
buildScatterDataGrouped(groups, xTest, yTest) ScatterPoint[] Deprecated. Same, with each point tagged group: string — every group's points are returned together (this function never restricts to one group).
buildFacetTable(items, options?) FacetField[] The distinct-values table over wafer.metadata — "what can I group/compare/split by?" One entry per metadata key present on at least one item, curated via DEFAULT_FACET_CURATION (lot, product, testProgram, temperature, split, operator, testDate; waferId is curated facet: false — present but not offered, since it's unique per item by definition). options.facetableOnly (default true) restricts to curated-facet:true-or-uncurated keys; pass false to include waferId too.
facetValueOf(metadata, key, curation?) string \| undefined The faceting value of one metadata key for one item — date-curated fields (testDate) truncate to date-only.
mergeTestDefs(items) { defs, conflicts, warnings } The ONE test list for a population of wafers. TestDef.testNumber identifies a test within a test program, so taking any single wafer's testDefs as the namespace for a multi-program load pools unrelated measurements under one number and normalises them against the wrong limits. This unions every test number across items and reconciles the defs describing each. An absent field is "not stated", never a disagreement — mixing a file that states limits with one that does not merges silently, the stated value winning. Only two stated and different values conflict, in two tiers. Hard (distinct names, distinct units, or testType 'P' vs 'F'): different measurements sharing a number, so the test is withheld from defs entirely — warnings carries code test-def-collision, severity error. Soft (same name and unit, both limits stated but different): the same measurement under different specs, so the test stays and its values still pool, but the merged def drops both limits — no Cp/Cpk/Pp/Ppk, no spec yield, no limit lines — with code test-limit-conflict, severity warning. Limits compare on a relative tolerance, not ===, so a float32 STDF limit and a float64 CSV one cannot manufacture a conflict; names compare trimmed and case-insensitively. Hand warnings straight to collectWarnings (§10.x) to surface both through the toolbar indicator and Summary banner. renderWaferGallery and the Insights tab call this internally — hosts need it only when building their own cross-wafer surface.

FACET_NONE_VALUE ('(none)') is the residual bucket: buildFacetTable emits it as a FacetValue.value for items whose metadata has no value for that field, so the counts still add up to the full population instead of silently dropping the gap. It always sorts last regardless of size. Compare against the constant, not the literal string — and treat it as "field missing", not as a real metadata value, when labelling anything a user reads. A wafer genuinely carrying the string "(none)" is indistinguishable from a missing one, which is the one case where this bucket is ambiguous.

WaferMetadata → §12.3

7.17 LotSummaryReportParams grouping detail

See §7.8 for the full renderLotSummaryReportHtml grouping behavior — items are partitioned by whichever of lot/product/testProgram/temperature actually vary, never by split alone.


8 Web Worker

buildWaferMap and the analysis functions can be moved off the main thread so a large build does not freeze the UI. The @wafertools/wafermap/worker subpackage provides a thin wrapper around a pre-built worker script.

The worker is a responsiveness tool, not a speed tool. The worker runs the same code as the main thread, then pays an additional cost: every WaferMapInput sent in and every WaferMapResult sent back is deep-copied by the structured-clone algorithm behind postMessage. For a result this copy can cost ~2× the build itself, and the deserialize half of it lands back on the main thread. So in total wall-clock time the worker is always slower than calling buildWaferMap directly. What you buy is that most of the work happens off-thread, so the page stays interactive instead of locking up.

When to use it: only when a single synchronous build would block the UI long enough to notice — roughly tens of thousands of dies, or many wafers built in one batch. Indicative figures (vary by machine and data):

dies per wafer main-thread build+analyze (blocks UI) worker wall-clock verdict
~500 ~7 ms ~12 ms don't use the worker — nothing to unblock
~20,000 ~275 ms ~370 ms use it if a ~¼s freeze matters
~50,000 ~810 ms ~1130 ms use it — a ~0.8s freeze is very visible

Below a few thousand dies the build is fast enough that the worker only adds latency. Don't reach for it by default. renderWaferMap always runs on the main thread regardless.

If you need both the result and its analysis, use runWithAnalysis (§8.5), not run followed by runAnalysis. The latter ships the large result out of the worker and clones it straight back in for analysis — three crossings of the big object instead of one.

8.1 Setup

import { createWafermapWorker } from '@wafertools/wafermap/worker';

// Bundler (Vite, webpack…) — import the worker script URL
import workerUrl from '@wafertools/wafermap/worker-script?url';
const worker = createWafermapWorker(new Worker(workerUrl, { type: 'module' }));

// Plain HTML / CDN
const worker = createWafermapWorker(
  new Worker('https://cdn.jsdelivr.net/npm/@wafertools/wafermap/dist/packages/worker/wafermap.worker.js', { type: 'module' })
);

Create the worker once and reuse it for all calls.

8.2 createWafermapWorker(worker)

createWafermapWorker(worker: Worker): WafermapWorker

Returns a WafermapWorker:

// WafermapWorker
{
  run(input: WaferMapInput): Promise<WaferMapResult>
  runAnalysis(
    results: WaferMapResult[],
    options: AnalyzeWaferMapOptions,
    hasMultiWafer: boolean,
  ): Promise<{ waferSummaries: StatsSummary[]; lotSummary: LotStatsSummary | null }>
  runWithAnalysis(
    inputs: WaferMapInput[],
    options: AnalyzeWaferMapOptions,
    hasMultiWafer: boolean,
  ): Promise<{ results: WaferMapResult[]; waferSummaries: StatsSummary[]; lotSummary: LotStatsSummary | null }>
  terminate(): void
}

WaferMapInput → §4.1 · WaferMapResult → §4.2

8.3 worker.run(input)

worker.run(input: WaferMapInput): Promise<WaferMapResult>

WaferMapInput → §4.1 · WaferMapResult → §4.2

Identical input and output to buildWaferMap — just async.

// Replaces:
const result = buildWaferMap({ results, waferConfig, dieConfig });

// With:
const result = await worker.run({ results, waferConfig, dieConfig });

// Everything after is unchanged:
renderWaferMap(container, result);

Multiple concurrent calls are safe — each resolves independently. Run wafers in parallel with Promise.all:

const waferResults = await Promise.all(
  waferIds.map(id => worker.run({ results: dataByWafer[id], dieConfig }))
);

8.4 worker.runWithAnalysis(inputs, options, hasMultiWafer)

worker.runWithAnalysis(
  inputs: WaferMapInput[],
  options: AnalyzeWaferMapOptions,
  hasMultiWafer: boolean,
): Promise<{ results: WaferMapResult[]; waferSummaries: StatsSummary[]; lotSummary: LotStatsSummary | null }>

Builds and analyses in a single round-trip. The built WaferMapResults are analysed inside the worker and never sent out just to be sent back, so the large result objects cross the worker boundary only once. Prefer this whenever you need both the maps and their statistics — it avoids two extra structured-clone copies per wafer compared with run + runAnalysis.

const { results, waferSummaries, lotSummary } = await worker.runWithAnalysis(
  waferIds.map(id => ({ results: dataByWafer[id], dieConfig, passBins: [1] })),
  {},
  waferIds.length > 1,
);
results.forEach((result, i) =>
  renderWaferMap(containers[i], result, { statsSummary: waferSummaries[i] }));

AnalyzeWaferMapOptions → §7.3 · StatsSummary → §7.4 · LotStatsSummary → §7.5

8.5 worker.terminate()

worker.terminate(): void

Shuts down the underlying worker. Any in-flight calls reject immediately.


9 Low-level canvas API

Deprecated — removed in 0.31.0, with the whole low-level pipeline (§11). Draw maps with renderWaferMap or renderWaferGallery.

toCanvas is the one-shot draw primitive that renderWaferMap is built on. You need it when: - building a custom rendering loop (custom zoom/pan, animation, non-standard hit-testing) - generating wafer map images server-side or in a headless environment

For all other use cases, renderWaferMap is simpler and handles DPI, resize, and interaction automatically.

9.1 toCanvas(canvas, view, options?)

toCanvas(canvas: HTMLCanvasElement, view: View, options?: ToCanvasOptions): { hitTarget: HitTarget; viewport: Viewport; binLegendRows: BinLegendRow[] }

View is returned by buildView (§11.15).

Renders a view directly onto an HTML <canvas> element using the 2D Canvas API. No toolbar is provided — this is a one-shot draw call.

import { toCanvas } from '@wafertools/wafermap/render';
interface ToCanvasOptions {
  padding?:         number    // CSS-px padding inside canvas edge (default 16)
  showColorbar?:    boolean   // draw colorbar / bin legend (default true)
  colorbarWidth?:   number    // CSS-px width of the colorbar strip (default 16)
  background?:      string    // canvas background colour (default '#f5f5f5')
  showAxes?:        boolean   // draw axis tick marks and labels (default false)
  showTitle?:       boolean   // draw the map title (test/mode name + stack context) by the colorbar/legend (default true)
  diePitchMm?:      { x: number; y: number }  // when provided, axis labels show die grid indices; otherwise mm values
  fallbackFormat?:  'si' | 'engineering'  // format for unitless values outside [0.1, 9999] (default 'engineering')
}

Legend behaviour by plot mode:

Mode Legend
value Continuous colorbar (gradient with min/max ticks). Under passFailDisplay: 'spec' the colorbar is replaced by a spec legend: Pass / Fail high / Fail low swatches with per-category die counts, adaptive to the active test's limits (a one-sided spec omits the absent fail side). Under passFailDisplay: 'test' (always the case for a functional active test) it is replaced by a Pass / Fail legend with per-category die counts.
stackedValues Continuous colorbar.
stackedBins, stackedSoftBins Continuous colorbar (counts).
hardBin, softBin Bin legend: one swatch + label per unique bin; overflows show "+ N more".

Map title (showTitle, default true): every mode draws a title naming what the map shows, placed by the colorbar/legend (never under the toolbar). The primary line sits above the scale, supporting context below it:

Mode Title (primary · secondary)
value Vth (mV)
value + passFailDisplay: 'spec' Vth (mV) · #1050 · Spec pass/fail
value + passFailDisplay: 'test' Vth (mV) · #1050 · Tester pass/fail — or Functional pass/fail for a functional test
stackedValues Vth (mV) · mean · stacked (6 wafers)
hardBin / softBin Hard Bin / Soft Bin
stackedBins / stackedSoftBins Hard Bin 2 · Leakage · stacked (6 wafers)

The colorbar also shows a scale note (log₁₀, or linear — log n/a when log was requested but the data range includes ≤ 0).

Returns { hitTarget, viewport, binLegendRows }:

  • hitTarget.getDieAtPoint(x, y): Die | null — hit-test a CSS-pixel position
  • viewport — the auto-fitted viewport transform (useful as initial state for custom zoom/pan)
  • binLegendRows{ bin, y, h }[] for hit-testing legend row clicks (non-empty for hardBin/softBin; the spec legend also populates this, using negative sentinel bin keys for its Pass/Fail rows)
import { buildView } from '@wafertools/wafermap/renderer';

const result  = buildWaferMap({ results, waferConfig, dieConfig });
const view   = buildView(result.wafer, result.dies, { plotMode: 'hardBin' });
const { hitTarget } = toCanvas(canvas, view);

canvas.addEventListener('mousemove', e => {
  const r   = canvas.getBoundingClientRect();
  const die = hitTarget.getDieAtPoint(e.clientX - r.left, e.clientY - r.top);
  if (die) showTooltip(die);
});

hitTarget is a HitTarget object.

toCanvas reads window.devicePixelRatio automatically and snaps canvas dimensions to integer CSS pixels to prevent sub-pixel interpolation blur. Set canvas size in CSS only; do not set canvas.width/canvas.height directly.

renderWaferMap additionally watches for devicePixelRatio changes (browser zoom, moving between displays) via a matchMedia listener and re-renders automatically.


10 Package surface

import { buildWaferMap }                       from '@wafertools/wafermap';
import { renderWaferMap }                      from '@wafertools/wafermap/render';
import { analyzeWaferMap, analyzeWaferLot }    from '@wafertools/wafermap/stats';
import { createWafermapWorker }                from '@wafertools/wafermap/worker';

The statistics engine (analyzeWaferMap, analyzeWaferLot, filterFindings) is also available from the /stats subpath and is re-exported from the root package. It has no DOM dependency, so you can run a complete build-and-analyse pipeline in Node.js:

// Node.js — no DOM required
import { buildWaferMap }   from '@wafertools/wafermap';
import { analyzeWaferMap } from '@wafertools/wafermap/stats';

Only renderWaferMap and toCanvas (both from /render) require a browser environment.

Which build is running — WMAP_VERSION / WMAP_BUILD_TIME

import { WMAP_VERSION, WMAP_BUILD_TIME } from '@wafertools/wafermap/render';
// '0.28.0'   '2026-09-11T07:58:39.023Z'

Both are generated at build time, so they describe the bundle actually loaded rather than whatever a nearby package.json claims — different things whenever a host is linked to a local checkout. Show them wherever your application reports its own version: when a map looks wrong, the first question is which engine produced it, and "check the browser console" is not a useful answer for an end user. tsmap lists both in its Help → About tsmap… dialog.

The renderers are not on the root entry point. renderWaferMap, renderWaferGallery and toCanvas are exported only from @wafertools/wafermap/render — so import { renderWaferMap } from '@wafertools/wafermap' will fail. This is deliberate: the root entry stays DOM-free (and therefore usable in Node and tree-shakeable) by re-exporting only core, renderer and stats. Import the geometry/data layer from the root and the renderer from /render.

buildView and View are no longer exported from the root @wafertools/wafermap package. They are available from @wafertools/wafermap/renderer for advanced use (see §11 Manual Pipeline). If you were importing them from the root, update your import path.

10.1 Helper exports

getDieTestValue, dieHasTestData, isParametricTest, metadataDisplayValue, metadataCategoricalValue and discoverDieMetadataKeys are deprecated — removed in 0.31.0. getDieKey and getTestPassStatus stay: they carry rules a host processing dies must match. Read a test value as die.testValues[testNumber].

import { getDieKey, getDieTestValue, getTestPassStatus, dieHasTestData, isParametricTest } from '@wafertools/wafermap';
getDieKey(die: { x: number; y: number }): string
getDieTestValue(die: Die, testNumber: number): number | undefined
getTestPassStatus(die: Die, testNumber: number, testDef?: TestDef): boolean | undefined
dieHasTestData(die: Die): boolean
isParametricTest(def: TestDef | undefined): boolean

getDieKey returns a stable "x,y" string for map lookups (see §11.17 for details). Die → §12.1

getDieTestValue reads a test value from a die by test number:

const idsat = getDieTestValue(die, 1050);

Returns undefined when no value is present. Use this in post-build code that reads test values from dies.

getTestPassStatus is the single read-path for "did this die pass test N" (true = pass). Primary source: die.testPass[testNumber]. Migration fallback — this is the only place the rule exists: for a functional test (testType: 'F') with no testPass entry but a testValues entry of exactly 0 or 1, the legacy encoding is interpreted as 1 = pass / 0 = fail. The fallback is never applied to parametric tests. Returns undefined when no verdict is recorded — treat that as no-data, never as a fail.

dieHasTestData is true when a die carries any per-test data — a test value or a recorded pass/fail verdict.

isParametricTest returns false only for testType: 'F' — an undefined def or undefined testType counts as parametric.

Metadata helpers — the same three functions the library uses internally to turn a raw die.metadata / wafer.metadata value into something displayable, exported so a host formats metadata the way the maps, tooltips, legends and CSV exports already do rather than reinventing the rules:

import { metadataDisplayValue, metadataCategoricalValue } from '@wafertools/wafermap';
import { discoverDieMetadataKeys } from '@wafertools/wafermap/stats';
metadataDisplayValue(raw: unknown): string | undefined
metadataCategoricalValue(raw: unknown): string | undefined
discoverDieMetadataKeys(
  dies: Die[],
  metadataFields?: MetadataFieldDef[],
  limit?: number,                        // default 64
): { keys: string[]; truncated: string[] }

metadataDisplayValue renders one value for display: strings, numbers and booleans stringify, a Date becomes an ISO string, anything else is JSON — and an unserialisable value (a circular structure, a BigInt) returns an honest marker rather than throwing out of a tooltip or an export. Empty string, null and undefined all return undefined, meaning "no value", which is not the same as "undefined".

metadataCategoricalValue is the same thing narrowed to values that can act as a category — it rejects objects and arrays, which cannot meaningfully colour a die or head a column. This is the derivation 'metadata' plot mode and the gallery's metadata legend both use, so a host that groups or tallies by a metadata key gets the same buckets the map paints.

discoverDieMetadataKeys (from /stats) returns the die-metadata keys actually present across a set of dies — what metadataColumns: 'auto' (§5.4.4) resolves to. Keys you declared in metadataFields come first, in the order you declared them, and the rest follow in natural order, so a host's own curation survives into the column layout. Anything past limit (default 64) is returned separately as truncated rather than dropped silently, so a caller can say what it left out. MetadataFieldDef → §4.1.11

10.2 ICONS

import { ICONS } from '@wafertools/wafermap/render';

ICONS: Record<string, string>   // icon key → inline SVG markup (uses currentColor for stroke/fill)

The toolbar's own icon set, keyed by name ('download', 'expand', 'close', 'help', …). A host rendering its own chrome (overlay buttons, custom toolbars) alongside wmap's can import ICONS to match wmap's iconography exactly, instead of copy-pasting SVG strings that silently drift on the next icon redesign. Each value is ready to assign to element.innerHTML.

Key names are stable once published — removing or renaming a key is a breaking change, same as any other public export; new keys may be added in a patch release. There is no separate "curated public subset" — the whole set is public, since it is just data (no behaviour to keep internal).

10.3 openReportModal

Deprecated — removed in 0.31.0. See §7.6.

import { openReportModal } from '@wafertools/wafermap/render';

openReportModal(html: string, opts?: { anchor?: Element; ownerDocument?: Document }): OverlayHandle

Opens report HTML (from renderFindingsReportHtml, renderSummaryReportHtml, or renderLotSummaryReportHtml) in an in-app modal. The Summary panel's "Summary report" button (§7.7, §7.8) calls this automatically — no setReportOpener wiring is required just to view a report anymore, in a plain browser tab or an embedded host (Tauri, Electron, WebView2) alike.

The modal's header includes a Print/Save-as-PDF button (window.print(), scoped to just the report — see below), and its content area has an "Open as full page ↗" link that falls back to openHtmlReport/setReportOpener (§7.9) for a host that wants the report as a real separate page instead — to keep it open outside the modal, or as a fallback if in-app printing misbehaves on a given platform. §7.9's setReportOpener registration, if a host has one, is what that link and any other openHtmlReport call route through; it is otherwise entirely optional now.

The report HTML is a complete standalone document (own <style>, own <!DOCTYPE html>) rendered via <iframe srcdoc> — its stylesheet is isolated from the host page automatically, and the print button targets iframe.contentWindow.print(), so only the report prints, not the host app around it. This works identically in a plain browser tab and inside a Tauri/Electron/WebView2 webview — printing is native window.print() either way, so this needs no platform-specific host code.

opts.anchor, when given, resolves the modal into the correct document/stacking root the same way every other overlay in this library does (e.g. a summary panel rendered inside a gallery card detached into its own window) — pass the triggering element (a button, the panel container) when in doubt.

10.4 openWaferMapGuide

import { openWaferMapGuide } from '@wafertools/wafermap/render';
import type { UserGuideExtension } from '@wafertools/wafermap/render';

openWaferMapGuide(extension?: UserGuideExtension, anchor?: Element): void

Opens the built-in guide window with no live WaferMapController/GalleryController requiredWaferMapController.openUserGuide()/GalleryController.openUserGuide() (§5.4, §6.2) are thin wrappers around this exact same call, built from the library's own top-level functions rather than anything derived from a specific render. Use this when a host's help entry point must also work before anything has been rendered yet (an empty-state "Help" menu, for example) — prefer the controller method when one already exists; this exists for the gap it can't cover.

extension, when given, behaves identically to userGuideExtension (§5.11) — host content is prepended before wmap's own guide content, in the same combined window. Pass the same extension object to both this call and every renderWaferMap/renderWaferGallery call so a host's "one Help entry point" always shows identical content regardless of whether a map is currently rendered.

anchor, when given, resolves the guide window into the correct document/owner-window the same way opts.anchor does above.

Available subpath exports: @wafertools/wafermap, /core, /renderer, /render, /stats, /worker, /worker-script

The Insights tab's canvas chart panels are not a public subpath. insights.enabled (§5.9, §6.10) is the supported way to get charts — the DOM/canvas rendering code behind it is internal to /render and not independently importable, so you cannot assemble your own page from wmap's chart panels the way you can compose /render's other pieces. The pure data layer those panels are built on (§7.16) is public from /stats, if you want to drive your own chart library from the same computations.


11 Advanced / Manual Pipeline

Deprecated — removed in 0.31.0. The manual pipeline is being withdrawn: nothing known uses it, and it doubled the API a host had to read. Build with buildWaferMap and draw with renderWaferMap or renderWaferGallery. In this section getDieKey (§11.18), isYieldEligibleDie (§11.20), the colour-scheme registries and resolveValueColorFn (§11.19) stay; everything else is deprecated, including resolveBinColors, getBinColorScheme and contrastTextColor. If you depend on any of it, say so at https://github.com/wafertools/wafermap/issues.

You only need this section if you are building a custom rendering pipeline — for example, rendering to SVG or WebGL, generating images server-side, or inserting custom geometry processing steps between wafer creation and rendering. For everything else, use buildWaferMap + renderWaferMap.

Minimal working example:

import { createWafer, generateDies, clipDiesToWafer } from '@wafertools/wafermap/core';
import { buildView } from '@wafertools/wafermap/renderer';
import { toCanvas } from '@wafertools/wafermap/render';

const wafer = createWafer({ diameter: 300 });
const dies  = clipDiesToWafer(generateDies(wafer, { width: 10, height: 10 }), wafer);
// attach your data: dies.forEach(d => { d.hbin = ...; })
const view  = buildView(wafer, dies, { plotMode: 'hardBin' });
toCanvas(canvas, view);

Full pipeline (with orientation and interactive transforms):

graph TD
    cw["createWafer(spec)"]
    gd["generateDies(wafer, dieSpec)"]
    cd["clipDiesToWafer(dies, wafer, dieSpec)"]
    av["attach values / hbin / sbin / metadata"]
    ps["applyProbeSequence(dies, config)"]
    ao["applyOrientation(dies, wafer)"]
    td["transformDies(dies, interactiveTransform, wafer.center)"]
    bv["buildView(wafer, dies, options)"]
    tc["toCanvas(canvas, view)"]

    cw --> gd --> cd --> av --> ao
    av -. "optional" .-> ps --> ao
    ao -. "on each redraw" .-> td
    td --> bv --> tc

In the manual pipeline, die.x and die.y are integer grid indices centred at the wafer origin, computed by generateDies.

11.1 createWafer(spec)

createWafer(spec: WaferSpec): Wafer

Creates a wafer model. diameter is required. Accepts a WaferSpec:

{
  diameter:     number                     // required
  center?:      { x: number; y: number }   // mm, default {0, 0}
  notch?:       { type: 'top' | 'bottom' | 'left' | 'right' }
  orientation?: number                     // degrees CW, default 0
  metadata?:    WaferMetadata
}

Returns Wafer with diameter, radius, center, notch (with computed length), orientation, metadata.


11.2 generateDies(wafer, spec)

generateDies(wafer: Wafer, spec: DieSpec): Die[]

Wafer → §12.2 · Die → §12.1

Creates a rectangular die grid centred on the wafer. Accepts a DieSpec:

{
  width:     number   // required
  height:    number   // required
  gridSize?: number
  offset?:   { x: number; y: number }
}

Returns Die[] with id, x (grid), y (grid), physX (mm), physY (mm), width, height.


11.3 clipDiesToWafer(dies, wafer, spec?)

clipDiesToWafer(dies: Die[], wafer: Wafer, dieConfig?: DieSpec): Die[]

Clips dies to the wafer boundary (circle + optional notch/flat exclusion zone).

  • Removes dies entirely outside the wafer.
  • Sets insideWafer: true on included dies.
  • Sets partial: true on dies that straddle the boundary (requires dieConfig for 4-corner test).

Die → §12.1 · Wafer → §12.2 · DieSpec → §11.2


11.4 isInsideWafer(x, y, wafer)

isInsideWafer(x: number, y: number, wafer: Wafer): boolean

Returns true when the point (x, y) in physical mm falls inside the wafer boundary.

Wafer → §12.2


11.5 mapDataToDies(dies, data, options)

mapDataToDies(dies: Die[], data: DataRow[], options: MapOptions): Die[]

Maps row data onto dies by matching grid coordinates. DataRow is Record<string, string | number>.

// MapOptions
{
  matchBy?:    'xy' | 'ij'   // field pair to match on: x/y or i/j (default 'xy')
  xField?:     string        // field name for x coordinate (default 'x')
  yField?:     string        // field name for y coordinate (default 'y')
  iField?:     string        // field name for i coordinate (used when matchBy = 'ij')
  jField?:     string        // field name for j coordinate
  valueField:  string        // field name for the value to attach
}

Die → §12.1


11.6 applyOrientation(dies, wafer)

applyOrientation(dies: Die[], wafer: Wafer): Die[]

Rotates die physical coordinates (physX, physY) by wafer.orientation (degrees CW) around wafer.center. Call once after clipDiesToWafer and before transformDies.

Die → §12.1 · Wafer → §12.2


11.7 transformDies(dies, options, center?)

transformDies(dies: Die[], options: TransformOptions, center?: { x: number; y: number }): Die[]

Applies interactive display transforms (rotation + flip) around center (defaults to wafer.center). Call on each redraw when the user rotates or flips.

// TransformOptions
{
  rotation?: number   // clockwise degrees: 0 | 90 | 180 | 270
  flipX?:    boolean
  flipY?:    boolean
}

Die → §12.1


11.8 applyProbeSequence(dies, config)

applyProbeSequence(dies: Die[], config: ProbeSequenceConfig): Die[]

Assigns probeIndex to each die according to the chosen scan strategy. Returns a new Die[] with probeIndex populated. Set showProbePath: true in buildView options to draw the path as an overlay.

// ProbeSequenceConfig
{
  type:          'row' | 'column' | 'snake' | 'custom'
  //   row    — left→right, top→bottom
  //   snake  — alternating direction per row (boustrophedon)
  //   column — top→bottom, left→right
  //   custom — explicit die ID ordering via customOrder
  customOrder?:  string[]   // ordered die IDs — required when type = 'custom'
}

Die → §12.1


11.9 generateReticleGrid(wafer, spec)

generateReticleGrid(wafer: Wafer, spec: ReticleSpec): Reticle[]

Wafer → §12.2

Generates reticle rectangles covering the wafer area. Accepts a ReticleSpec:

{
  width:       number
  height:      number
  diePitchX:   number
  diePitchY:   number
  anchorDie?:  { x: number; y: number }
               // die index at the field's min-x/min-y corner (bottom-left,
               // since +Y is up) — see §4.1.4
  gridOrigin?: { x: number; y: number }
               // physical position of die index (0,0). Default {0,0} (the
               // wafer centre) — correct only when the die grid is itself
               // centred on the wafer. `buildWaferMap` passes this
               // automatically; pass it yourself only when calling
               // generateReticleGrid directly against a die grid whose
               // physical placement is offset from the wafer centre by a
               // non-whole-die-pitch amount (e.g. partial/off-centre data),
               // otherwise field boundaries silently drift off die edges.
}

Via buildWaferMap, pass reticleConfig: ReticleConfig instead — pitch is wired through automatically.


11.10 classifyDie(die, wafer, options?)

classifyDie(die: Die, wafer: Wafer, options?: { ringCount?: number }): { ring: number; quadrant: 'NE' | 'NW' | 'SW' | 'SE' }

ring runs 1 (innermost) to ringCount (outermost/edge). Default ringCount is 4.

Die → §12.1 · Wafer → §12.2


11.11 getRingLabel(ring, ringCount)

getRingLabel(ring: number, ringCount: number): string

Returns a human-readable label for a ring index, e.g. "Ring 1 (centre)", "Ring 4 (edge)".


11.12 getUniqueBins(dies, binSpace?)

getUniqueBins(dies: Die[], binSpace?: 'hard' | 'soft'): number[]

Returns all distinct bin values present in dies, sorted ascending. binSpace selects which field to read: 'hard' reads die.hbin (default), 'soft' reads die.sbin.

Die → §12.1


11.13 aggregateBinCounts(diesByWafer, targetBin, binSpace?)

aggregateBinCounts(diesByWafer: Die[][], targetBin: number, binSpace?: 'hard' | 'soft'): Die[]

Stacks multiple wafers and counts, per die position, how many wafers had a specific bin value.

Returns one Die per unique (x, y) with testValues[0] = count, and hbin: targetBin (for 'hard') or sbin: targetBin (for 'soft').

  • Pass binSpace: 'hard' (default) for hard bins → use with plotMode: 'stackedBins'
  • Pass binSpace: 'soft' for soft bins → use with plotMode: 'stackedSoftBins'

Set valueRange: [0, diesByWafer.length] and lotSize: diesByWafer.length for correct colorbar and percentage tooltips.

Die → §12.1


11.14 aggregateValues(diesByWafer, method, paramIndex?)

aggregateValues(diesByWafer: Die[][], method: 'mean' | 'median' | 'stddev' | 'min' | 'max' | 'count', paramIndex?: number): Die[]

paramIndex — the testValues key to read from each source die (e.g. a testNumber like 1050). Defaults to 0.

Returns one Die per unique (x, y) with the aggregated scalar stored at testValues[0], ready for buildView in stackedValues mode.

Die → §12.1


11.15 buildView(wafer, dies, options?)

buildView(wafer: Wafer, dies: Die[], options?: ViewOptions): View

Builds the renderer-agnostic view. Wafer → §12.2 · Die → §12.1

interface ViewOptions {
  plotMode?:               'value' | 'hardBin' | 'softBin' | 'stackedValues' | 'stackedBins' | 'stackedSoftBins' | 'metadata'
  passFailDisplay?:        'off' | 'spec' | 'test'  // solid pass/fail display: 'spec' = limits judgement, 'test' = recorded verdict (die.testPass);
                                             // library-resolved — degrades to 'off' when invalid; functional tests always render as 'test'
  showDieLabels?:          boolean
  showPartialDies?:        boolean   // default true; set false to hide edge dies outside the wafer circle
  showReticle?:            boolean
  showProbePath?:          boolean
  showRingBoundaries?:     boolean
  showQuadrantBoundaries?: boolean
  showXYIndicator?:        boolean
  ringCount?:              number    // default 4
  binColorScheme?:         string    // bin palette for hardBin/softBin, default 'default' — §11.19
  valueColorScheme?:       string    // value gradient for value/stacked modes, default 'default'
  reverseValueScheme?:     boolean   // flip that gradient end-for-end, default false
  useDefinedBinColors?:    boolean   // honour BinDef.color, default true
  binColors?:              BinColors // colours resolved over a wider population (every wafer in a gallery); ignored unless it covers every bin here
  highlightBin?:           number
  highlightMetadataValue?: string    // 'metadata' mode's analogue of highlightBin
  valueRange?:             [number, number] | { test: number; range: [number, number] }
  interactiveTransform?:   { rotation?: number; flipX?: boolean; flipY?: boolean }
  reticles?:               Reticle[]
  testDefs?:               TestDef[]   // named test definitions — drives mode dropdown and tooltip labels
  hbinDefs?:               BinDef[]    // named hard bin definitions (hbin, 0–32767 space)
  sbinDefs?:               BinDef[]    // named soft bin definitions (sbin, 0–32767 space — independent)
  metadataFields?:         MetadataFieldDef[]  // opts die.metadata keys into 'metadata' plot mode — §4.1.11
  activeTest?:              number      // testNumber to display in 'value' mode (matches testDef.testNumber, NOT a positional index); defaults to first available test
  activeMetadataKey?:      string      // die.metadata key to display in 'metadata' mode (matches a metadataFields[].key)
  logScale?:               boolean     // override log₁₀ scale for the active test; takes precedence over TestDef.logScale
  colorbarRangeMode?:      'spec' | 'data'  // sets colorbar range only: 'spec' (default) spans [limitLow, limitHigh],
                                            // 'data' spans actual data min/max. In both, dies keep the gradient fill and
                                            // out-of-spec dies get a triangle marker (▽ below limit, △ above)
  aggregationMethod?:      string      // aggregation method label for 'stackedValues' hover tooltips (e.g. 'mean', 'median')
  lotSize?:                number      // total wafers in lot — for 'stackedBins'/'stackedSoftBins' hover percentage computation
}

Returns View with rectangles, texts, overlays, hoverPoints, plotMode, binColorScheme, valueColorScheme, binColors (every bin's resolved colour, both bin types — read this for any legend or chart, never re-derive a colour), metadata, dies, valueRange, testDefs, hbinDefs, sbinDefs, activeTest, logScale, aggregationMethod, lotSize, and (for 'metadata' mode) metadataFields, activeMetadataKey, metadataCounts (Map<string, number> — value → die count, mirroring binCounts).

Display-transform fields:

  • gridToScreen (Affine<'grid','screen'>, §11.21) — the authoritative die-grid → screen transform. Use this (or affineInvert of it) for anything positional; it is composed from the real pipeline in order and stays correct for every combination of wafer orientation, data-axis flip and interactive rotate/flip.
  • dataAxisFlip — the data-pipeline flip alone (from xAxisDirection/yAxisDirection/coordinateOrigin), with no interactive flip mixed in.
  • axisFlip / rotation — a lossy summary (total flip as an XOR, total rotation as a sum). These cannot represent rotate → mirror → rotate and are only exact while wafer.orientation is 0; they remain for coarse orientation hints. Prefer gridToScreen for coordinate maths.

hoverPoints is { x, y }[] — one entry per die, in physical mm coordinates. Used internally by renderWaferMap for hit-testing; you rarely need it directly when using toCanvas (use hitTarget.getDieAtPoint instead).


11.16 buildHoverText(die, plotMode, opts?)

buildHoverText(
  die:      Die,
  plotMode: PlotMode,
  opts?: {
    testDefs?:       TestDef[]
    hbinDefs?:       BinDef[]
    sbinDefs?:       BinDef[]
    fallbackFormat?: 'si' | 'engineering'
    aggrMethod?:     string                 // lot-stack aggregation method name
    lotSize?:        number                 // wafers in the stack — annotates a count with its lot share
    activeTest?:     number                 // active test number (value mode) — leads the tooltip
    reticleConfig?:  ReticleConfig          // when set, adds "Reticle (column, row)" below "Die (x, y)"
  },
): string

Builds the HTML tooltip string for a single die. Exported so custom toCanvas pipelines can generate the same tooltip content as renderWaferMap without re-implementing the formatting logic.

The tooltip is compact and mode-aware, so it never becomes an unwieldy block on dies with many tests:

  • value mode — leads with the active test (activeTest) value, in bold, with an (out of spec) note when it fails its limits; the remaining tests are summarised as +N more tests.
  • hardBin / softBin mode — shows the bin verdict (HBin / SBin), then a N test values recorded count rather than listing individual tests (no single test is privileged in bin mode).
  • stacked modes — show the single aggregated value.

When reticleConfig is passed, a Reticle (column, row) line is inserted immediately after Die (x, y), computed via getReticleCell (§11.21) — the same function buildReticlePositionRegions (§7) uses for reticle-position findings, so the tooltip and findings labels always agree. Omitted entirely when reticleConfig is not passed. renderWaferMap/renderWaferGallery pass this automatically from result.reticleConfig whenever a reticle was configured on buildWaferMap — no action needed for the built-in tooltip.

Die → §12.1 · TestDef → §4.1.8 · BinDef → §4.1.9 · ReticleConfig → §4.1.4

import { buildHoverText } from '@wafertools/wafermap/renderer';

const html = buildHoverText(die, 'hardBin', { testDefs, hbinDefs, sbinDefs });
tooltipEl.innerHTML = html;

Called automatically by renderWaferMap on hover. Only needed when building a custom rendering pipeline with toCanvas.


11.17 buildMapTitle(view, fallbackFormat?, binDefs?)

buildMapTitle(
  view:            View,
  fallbackFormat?: 'si' | 'engineering',   // default 'engineering'
  binDefs?:        BinDef[],               // active bin defs — names a single-bin stacked card's bin
): MapTitleParts   // { primary: string; secondary: string }

Builds the on-canvas map title for any plot mode, derived from the View. Returns a primary/secondary split so the renderer can place the key identifier above the colorbar/legend and supporting context (stack/wafer-count, or Spec pass/fail) below it. toCanvas calls this automatically when showTitle is true; exported so custom pipelines can render the same title. See the title table under §9.1.

View → §11.15 · BinDef → §4.1.9


11.18 getDieKey(die)

getDieKey(die: { x: number; y: number }): string

Returns a stable "x,y" string key for a die. Always prefer this over ad-hoc template literals — it guarantees a consistent format across grid offset corrections.

const map = new Map(result.dies.map(d => [getDieKey(d), d]));
const die = map.get(getDieKey({ x: 3, y: -2 }));

11.19 Color helpers

Signature Returns Description
valueToViridis(t: number) string Deprecated. Use resolveValueColorFn('default'), which returns the same colours.
valueToGreyscale(t: number) string Deprecated. Use resolveValueColorFn('greyscale'), which returns the same colours.
contrastTextColor(cssColor: string) '#000000' \| '#ffffff' Returns the WCAG-contrast text colour for a given background

Bin colours — resolveBinColors(dies, options?)

resolveBinColors(dies: Iterable<Die>, options?: {
  passBins?:            number[]    // default [1] — must match the passBins behind the yield figure
  binColorScheme?:      string      // registered bin palette, default 'default'
  hbinDefs?:            BinDef[]
  sbinDefs?:            BinDef[]
  useDefinedBinColors?: boolean     // honour BinDef.color, default true
}): BinColors

// BinColors
{
  hard:   Map<number, string>                  // hard bin → CSS colour
  soft:   Map<number, string>                  // soft bin → CSS colour (independent number space)
  shared: { hard: number[]; soft: number[] }   // bins drawn in a colour another bin also has
  pass:   { hard: Set<number>; soft: Set<number> }  // bins that pass: hard = in passBins; soft = every die passes
}

The one rule for bin colour, used by the map, its legends, the summary panels and the Insights charts. A bin's colour depends on its number and its pass/fail verdict — never on how many dies it has — so a bin is the same colour in every lot and every screenshot of a program:

  • Pass/fail follows passBins. Passing bins take the palette's pass colours (greens) and failing bins its fail colours, so with passBins: [1, 3] bin 3 is green and a failing bin 1 is not. A soft bin passes when every die carrying it passes (by the same per-die rule as yield, diePassStatus); otherwise, or when no die says, it takes a fail colour.
  • Keyed by bin number. A pass bin takes pass[(bin − 1) mod n] and a fail bin fail[(bin − 2) mod n], so bin 1 takes the first pass colour and bin 2 the first fail colour (red, in 'default'). The slot comes from the number alone, not the bin's position among the pass or fail bins, so changing passBins recolours only the bins whose verdict changed.
  • Soft bins read the palette shifted by half its length, so hard bin n and soft bin n are different colours.
  • BinDef.color wins over the palette for that bin.
  • shared lists bins that cannot be told apart by colour — two bins present whose numbers are a palette-length apart (fail bins 2 and 21 in 'default'), or a defined colour that repeats one. The renderers raise a bin-colors-shared warning (§4.4) when the bin map on screen has any.

renderWaferMap resolves over its own dies; renderWaferGallery resolves once over every wafer it shows and passes the result to every card as ViewOptions.binColors, so a bin is the same colour on every wafer. The result is exposed as View.binColors — read it rather than resolving again.

Colour scheme registries

Bin palettes and value gradients are separate registries, chosen by binColorScheme and valueColorScheme.

registerBinColorScheme(name: string, scheme: BinColorScheme): void      // throws on an empty pass or fail list
getBinColorScheme(name?: string): BinColorScheme                        // falls back to 'default'
listBinColorSchemes(): Array<{ name: string; label: string }>

registerValueColorScheme(name: string, scheme: ValueColorScheme): void
getValueColorScheme(name?: string): ValueColorScheme                    // deprecated — see below
listValueColorSchemes(): Array<{ name: string; label: string }>
resolveValueColorFn(name?: string, reversed?: boolean): (t: number) => string

Built-in gradients all read low = dark, high = light — the direction matplotlib and seaborn define them with, and the one that puts the rare end of a map at the bright end: on a stacked map the healthy bulk of the wafer sits back as dark ground while an edge ring or scratch lights up. 'traffic' and 'jet' are the exceptions, and are not lightness ramps at all.

Use resolveValueColorFn, not getValueColorScheme().forValue, anywhere you colour by value. It is the one place reverseValueScheme is applied, so a legend, chart or export built on it can never show a reading as a different colour than the map does. Pass View.valueColorScheme and View.reverseValueScheme together — they always travel as a pair. getValueColorScheme, valueToViridis and valueToGreyscale are deprecated for this reason and will be removed in 0.31.0; each still works and logs one console notice on first use. Use listValueColorSchemes() for scheme names and labels.

// BinColorScheme
{
  label: string              // shown in the toolbar Palette menu in bin modes
  pass:  readonly string[]   // colours for passing bins, from bin 1 — greens by convention
  fail:  readonly string[]   // colours for failing bins, from bin 2 — most distinct first,
                             // and never resembling a pass colour
}

// ValueColorScheme
{
  label:    string                   // shown in the Palette menu in value/stacked modes
  forValue: (t: number) => string    // CSS colour for a normalised value t ∈ [0,1]
}

A registered scheme appears in the matching toolbar menu automatically. Register once at startup, before rendering; the registries are global for the life of the page. The two built-in bin palettes were selected by measurement: 'default' keeps every pair of its 3 pass and 19 fail colours at least 16 ΔE00 apart, and 'accessible' keeps its 2 pass and 14 fail colours at least 8.8 ΔE00 apart under simulated deuteranopia, protanopia and tritanopia together (tests/binPalettes.test.mjs re-measures both).

11.20 isYieldEligibleDie(die, options?)

import { isYieldEligibleDie } from '@wafertools/wafermap';
// also available from '@wafertools/wafermap/core'

isYieldEligibleDie(die: Die, options?: { includePartial?: boolean; includeEdgeExcluded?: boolean }): boolean

Whether a die counts toward yield/rollup calculations, per wmap's standard fab-reporting convention: partial (boundary-straddling) and edgeExcluded dies are skipped by default, even though they may carry real measured values — many fabs exclude them from yield/bin reporting specifically, not from other per-die analysis (a partial/edge-excluded die's test values still belong in distributions, correlations, and scatter plots). Both options default to false (excluded). The two are not equally common in practice: edgeExcluded is live for any results-based map where the caller opted into waferConfig.edgeExclusion, while partial is only ever set on a synthesized grid built via clipDiesToWafer — a results-based map has none, so includePartial is a no-op there (§12.1).

This is the single source of truth for the rule — buildWaferMap's yield calculation, analyzeWaferMap's eligible-die filter, and the §7.16 chart-data builders' yield/bin-pareto functions all call it, so they never silently drift apart on which dies count.

Die → §12.1


11.21 Affine display transforms

import {
  affineIdentity, affineRotation, affineMirror, affineCompose,
  affineInvert, affinePoint, affineVector, affineSwapsAxes,
} from '@wafertools/wafermap';
// also available from '@wafertools/wafermap/core'

interface Affine<From extends CoordFrame, To extends CoordFrame> {
  a: number; b: number; c: number; d: number; e: number; f: number
}
// applied as:  x' = a·x + c·y + e     y' = b·x + d·y + f

type CoordFrame = 'physical' | 'grid' | 'baked' | 'screen'

Every rotation and mirror in the library is composed through this one type. It exists because rotation and mirroring do not commute: the real display pipeline is rotate → mirror → rotate → mirror (wafer orientation → data-axis flip → interactive rotation → interactive flip), and

mirror ∘ rot(θ) = rot(−θ) ∘ mirror

so collapsing that sequence into a single summed angle plus XOR'd flip flags is valid only when the first rotation is zero. Matrices compose associatively and get it right by construction.

function purpose
affineIdentity() the identity transform
affineRotation(deg, cx?, cy?) clockwise rotation about a centre — matches rotatePoint
affineMirror(flipX, flipY, cx?, cy?) mirror through a centre on each requested axis
affineCompose(outer, inner) inner first, then outer; frame tags must meet
affineInvert(m) inverse — e.g. screen → grid for axis labels
affinePoint(m, x, y) transform a position (translation applies)
affineVector(m, dx, dy) transform a direction (translation ignored)
affineSwapsAxes(m) whether an axis-aligned rect must exchange width/height

The From/To tags are phantom (type-level only; the runtime value is a plain 6-number object). They make composing in the wrong order, or applying a matrix to a point from the wrong frame, a compile error. The frames are:

  • physical — wafer-local mm for a physical wafer feature (boundary outline, notch, ring/quadrant borders). Follows wafer.orientation but not the data-axis flip: that flip exists to make the render physically truthful for a prober convention (yAxisDirection: 'down' puts row 1 at the top), so the wafer holds still while the die grid moves relative to it.
  • grid — wafer-local mm for geometry aligned to the die grid (reticle fields, the +X/+Y indicator). Does follow the data-axis flip, so it stays locked to the dies.
  • baked — what Die.physX/physY holds after buildWaferMap.
  • screen — final display mm after the interactive rotate/flip; what View.hoverPoints holds.

Use these when a custom toCanvas pipeline draws its own overlays: take view.gridToScreen (§11.15) and transform your geometry through it rather than re-deriving rotation and flip by hand.

import { affinePoint, affineInvert } from '@wafertools/wafermap';

// place a marker that must sit on a specific die-grid position
const p = affinePoint(view.gridToScreen, gridX, gridY);

// go the other way: screen mm → die-grid mm
const back = affinePoint(affineInvert(view.gridToScreen), p.x, p.y);

11.22 getReticleCell(die, config)

import { getReticleCell } from '@wafertools/wafermap';
// also available from '@wafertools/wafermap/core'

getReticleCell(
  die:    { x: number; y: number },
  config: { width: number; height: number; anchorDie?: { x: number; y: number } },
): { column: number; row: number }

The field-local (column, row) a die occupies within its reticle field, per the anchorDie convention documented on generateReticleGrid (§11.9): anchorDie is the field's min-x/min-y (bottom-left) corner, so a die's local position is die − anchorDie, wrapped to the field dimensions. Both column and row are 0-indexed.

This is the single source of truth for "which reticle cell is this die in" — buildReticlePositionRegions (§7) and buildHoverText's (§11.16) Reticle (column, row) tooltip line both call it, so field geometry, findings labels, and tooltip text can never drift apart.

Note this is purely index arithmetic on die.x/die.y, so it is unaffected by any display convention — xAxisDirection, coordinateOrigin, wafer orientation, and interactive rotate/flip change only where a die is drawn (see §11.21), never which reticle cell it belongs to.

ReticleConfig → §4.1.4


11.23 resolveGridPitch(gridPoints, dieOpts?, waferDiameter?)

import { resolveGridPitch } from '@wafertools/wafermap';
// also available from '@wafertools/wafermap/core'

resolveGridPitch(
  gridPoints:     Array<{ x: number; y: number }>,
  dieOpts?:       { width?: number; height?: number },
  waferDiameter?: number,
): {
  pitchX:     number
  pitchY:     number
  units:      'mm' | 'normalized'   // 'normalized' when no physical dimension was known
  confidence: number
}

The exact pitch derivation buildWaferMap uses internally, exposed so a host doing its own pre-flight on prober coordinates gets the same answer rather than re-deriving it (a host-side re-derivation is how grid geometry silently diverges from what the map renders).

gridPoints are integer prober step coordinates, not mm — physical position is grid_pos × pitch. When both die dimensions are supplied the result is exact and in mm. Otherwise nearest-neighbour step analysis is tried first (reliable for regular grids with ≥ 4 points), falling back to the circular-wafer aspect-ratio constraint; when no physical dimension is known at all, units is 'normalized' and confidence drops accordingly.

Check confidence before trusting the result, and prefer result.warnings (§4.2) for judging whether the geometry wmap actually built is sound — this helper answers "what pitch do these coordinates imply", not "is my configuration valid", which the library owns.

This is the one deliberate exception to inference/ being internal; nothing else from that directory is public.


12 Important types

12.1 Die

{
  id:            string
  x?:            number    // die grid X position — prober step coordinate (equals input x for centred grids)
  y?:            number    // die grid Y position — prober step coordinate (equals input y for centred grids)
  physX?:        number    // physical X in mm (or normalized units)
  physY?:        number    // physical Y in mm (or normalized units)
  width:         number    // die width in mm (or normalized units)
  height:        number    // die height in mm (or normalized units)
  testValues?:   Record<number, number>  // test measurements keyed by test number
  hbin?:         number    // hard bin (physical sort result; STDF V4 range 0–32767)
  sbin?:         number    // soft bin (test-program failure category; independent 0–32767 space)
  metadata?:     DieMetadata
  insideWafer?:  boolean
  partial?:      boolean   // straddles the wafer boundary — always false for dies
                           // built from `results` (see note below)
  edgeExcluded?: boolean   // centre falls within the edge exclusion zone
  probeIndex?:   number
  retestCount?:  number    // set when this position appeared more than once in input results
}

A die with test results is always fully on the wafer. A prober can only step to sites that lie entirely on the wafer, so a prober map never contains edge-straddling dies. buildWaferMap therefore never sets partial on a die built from results, and floors the inferred wafer diameter so it always contains every die.

It follows that a die falling outside the wafer boundary is proof the geometry is wrong, not the die — the measured positions are ground truth and the inferred diameter/pitch is the guess. If you supply waferConfig.diameter and it cannot contain the probed dies, wmap does not silently resize it (you asserted it); it adds an entry to result.inference.warnings naming the shortfall and the likely cause. The most common cause is supplying diameter without dieConfig.width/height: the pitch is then derived as diameter ÷ gridSpan, which assumes your data spans the full wafer — wrong whenever edge dies are absent. Supply the die pitch, which is the value that actually matters.

partial remains meaningful for a synthesized die grid clipped to a wafer — see clipDiesToWafer (§11.3), where straddling dies legitimately arise.

A die can have no reported position at all. x/y/physX/physY are optional for exactly this case — real-world data sometimes has no spatial layout (wafer-number-only logs), or a lot where some wafers have positions and others don't, including a single wafer mixing both. A die is either fully positioned or fully unpositioned, never half (buildWaferMap throws if only one of x/y is supplied). Use the isPositionedDie(die) type guard (@wafertools/wafermap/core) to narrow to PositionedDie (the same shape, x/y/physX/physY required) before calling anything that assumes a position — its looser sibling hasPosition(die) narrows only x/y, for inputs such as a raw DieResult that have no physX/physY to narrow — every spatial function in this library (region builders, cluster/pattern detection, buildView) already takes PositionedDie[], not Die[], so the type system catches a missing filter at compile time. getDieKey(die) falls back to `id:${die.id}` for an unpositioned die, so two of them never collide on the same key. Non-spatial consumers (yield, bin counts, per-test stats) are unaffected — they never read position and see coordinate-less dies like any other.

renderWaferMap/renderWaferGallery render a coordinate-less wafer as a die-list table or a compact bin/value summary in place of the map, never as a wafer-shaped mosaic with fabricated positions — see the end-user guide's "Dies with no reported position" section for what a host actually sees, and dataCoverage.unpositionedDies (§7) below for how a card's build result reports how many.

12.2 Wafer

{
  diameter:    number
  radius:      number
  center:      { x: number; y: number }
  notch?:      { type: 'top' | 'bottom' | 'left' | 'right'; length: number }
               // length = standard chord/half-width in mm, derived from diameter
  orientation: number
  metadata?:   WaferMetadata
}

12.3 WaferMetadata

Named fields with an open index signature — any extra key is accepted and displayed alongside the named fields in the summary panel header.

{
  lot?:         string
  waferId?:     string | number
  product?:     string
  testDate?:    string          // ISO 8601 recommended, e.g. "2026-04-23T08:30:00Z"
  operator?:    string
  testProgram?: string
  temperature?: number          // chuck temperature in °C
  split?:       string          // user-assigned experiment/process-corner tag (e.g. "TT", "FF"),
                                 // distinct from any parser-derived field — a first-class slot so hosts that
                                 // support wafer-split assignment get it picked up by the Insights tab's
                                 // "Group by" (§6.10) and lot summary reports' Splits section (§7.8) automatically
  [key: string]: unknown        // custom fields — shown in summary panel header
}

Custom fields are added at the top level, exactly like the named fields:

waferConfig: {
  metadata: {
    lot: 'LOT123', waferId: 1, testDate: '2026-04-23',
    equipmentId: 'P-01',  // custom — displayed in summary panel header
    recipe: 'NMOS-R2',    // custom
  }
}

Since 0.15.0, WaferMetadata is also the home for lot/wafer facts shown in die hover tooltips — they merge in as the tooltip base, with any per-die DieMetadata key overriding the wafer value.

12.4 DieMetadata

An open index signature for annotations that genuinely vary die-to-die. Any key is accepted and rendered automatically in die hover tooltips.

{
  [key: string]: unknown   // per-die fields — shown in hover tooltip automatically
}

Changed in 0.15.0: the named wafer-level fields (lotId, waferId, deviceType, testProgram, temperature) were removed from DieMetadata. They are properties of the wafer, not the die — set them once on WaferMetadata (waferConfig.metadata). Storing them per die replicated identical values across every die for no benefit.

The tooltip merges the wafer's WaferMetadata (base) with the die's DieMetadata; a per-die key overrides the wafer value of the same name. Both render as Label: value lines, skipping null/undefined — the label is metadataFields[].label when declared, else a Title-Cased version of the key (prettyKey), matching the die-list/CSV column labels (§5.4.4) rather than the raw key. wmap renders whatever keys the host supplies — it has no opinion on which fields belong in a tooltip, so control over tooltip content lives in the host-provided metadata.

Every metadata key also reaches the die-list table and its CSV export (§5.4.4) — the tooltip is not a special surface. Die metadata is on by default, one column per key; wafer metadata is CSV-only by default, so a detached export stays self-describing without cluttering the on-screen table.

// Wafer-level facts — set once:
buildWaferMap({
  results,
  waferConfig: { metadata: { lot: 'LOT-001', product: 'NMOS-A', testProgram: 'NM_v3.2' } },
});

// Per-die annotations — only what varies die-to-die:
{
  x: Number(r.x), y: Number(r.y), hbin: Number(r.hbin),
  metadata: { probeCard: 'PC-42', inkDate: r.inkDate },
}

// In onClick or onHover callback:
onClick: (die) => {
  const probeCard = die.metadata?.probeCard;
}

12.5 Other exported type names

Every type below is reachable from a function documented above — these are the names to import type when you need to annotate a variable rather than let inference do it. Their shapes are described at the function that produces or consumes them; this table exists so the name is discoverable, not to restate the shape.

The *Item types are input shapes for the §7.16 chart builders (what you pass in); the *Datum / *Data types they return are documented there. All accept dies?: Die[] and, where noted, precomputed statistics that are used in preference to re-walking dies.

Type Subpath Role
WaferNotch /core { type: 'top' \| 'bottom' \| 'left' \| 'right' }WaferConfig.notch.
Quadrant /core 'NE' \| 'NW' \| 'SW' \| 'SE'.
DieClassification /core classifyDie result — { ring, quadrant }.
ClassifyOptions /core classifyDie options — { ringCount? }.
DieEligibilityOptions /core isYieldEligibleDie options — includePartial? / includeEdgeExcluded?.
DieLike /core Minimal die shape the aggregation helpers accept — satisfied by both Die and DieResult.
AggregationMethod /core 'mean' \| 'median' \| 'stddev' \| 'min' \| 'max' \| 'count'.
PitchResult /core resolveGridPitch return — §11.23.
WaferMapOptions /renderer buildWaferMap's second argument — ViewOptions plus debug?.
ViewRect /renderer One drawn die rectangle on View.rects.
ViewText /renderer One positioned text run on the View.
ViewOverlay /renderer One overlay polyline set — wafer boundary, reticle, probe path, ring/quadrant boundary, XY indicator.
ViewHoverPoint /renderer { x, y } hover probe point.
ToCanvasResult /render toCanvas return — hitTarget, fitted viewport, binLegendRows.
ViewportTransform /render Pan/zoom state — { originX, originY, ppm, snapDist }. Usable as renderWaferMap's initial viewport.
InsightsView /render 'overview' \| 'distributions' \| 'correlation'.
DetachWindowOpener /render (label) => Window \| nullsetDetachWindowOpener, for hosts where window.open is blocked.
DieListOptions /render buildDieListSection options — §5.4.4.
DieListDisplayOptions /render Subset of DieListOptions reachable via RenderOptions.dieList / GalleryOptions.dieList — §5.4.4.
AnalyzeWaferMapInput /stats WaferMapInput \| WaferMapResult.
AnalyzeWaferLotInput /stats Array<WaferMapInput \| WaferMapResult>.
HighlightRegionTarget /stats StatsFinding.highlight variant kind: 'region'.
HighlightBinTarget /stats Variant kind: 'bin'.
HighlightWaferTarget /stats Variant kind: 'wafer'.
HighlightDieTarget /stats Variant kind: 'dies'.
MetadataColumn /stats resolveMetadataColumns result element — one table/CSV column.
MetadataColumnScope /stats 'die' \| 'wafer' — where a MetadataColumn's value comes from.
MetadataKeySelection /stats 'auto' \| 'none' \| string[]DieListDisplayOptions.metadataColumns.
ResolveMetadataColumnsOptions /stats resolveMetadataColumns options.
ParsedRegionKey /stats parseRegionKey return — { family, ring?, quadrant?, sector? }.
BinItem /stats Input to buildBinParetoData / buildBinClusterData; may carry precomputed hardBinCounts/softBinCounts.
BinType /stats 'hbin' \| 'sbin'.
BoxplotItem /stats Input to buildTestBoxplotData; may carry precomputed testStats.
CapabilityItem /stats Input to buildCapabilityData — one item is the short-term subgroup for Cp/Cpk.
BoxplotDatum /stats buildTestBoxplotData row — the five-number summary plus count.
TrendItem /stats Input to buildTestTrendData; like BoxplotItem, may carry precomputed testStats.
TrendDatum /stats One trend point — { label, mean, stddev, count, key? }. mean is NaN when count is 0.
TestPassRateItem /stats Input to buildTestPassRateData — one item within a group.
TestPassRateData /stats buildTestPassRateData return — { groups, rows, disagreementDies }. disagreementDies is null, not 0, when only one pass/fail source exists.
TestPassRateRow /stats One test's row across the groups.
TestPassRateValue /stats One group's cell in a row — absent where that group never ran the test.
TestPassKind /stats 'spec' \| 'testFlag' \| 'functional' — which pass/fail notion buildTestPassRateData measures.
HistogramItem /stats Input to buildTestHistogramData.
HistogramSeries /stats One group's counts in HistogramSeriesData, aligned to the shared ranges.
ScatterItem /stats Input to buildScatterData / buildScatterDataGrouped.
RegionYieldDatum /stats buildRegionYieldData row — key parses with parseRegionKey.
YieldSortBy /stats 'yield' \| 'label'.
CorrelationCell /stats One matrix cell — { xIndex, yIndex, r, n } (r is null on insufficient data). n is the dies carrying a finite value for both tests, which is not the population size when the two tests have different coverage — an r without its own n is not interpretable.
CorrelationTestInfo /stats A matrix axis entry — { testNumber, label, unit? }.
CorrelationSummary /stats filterCorrelationMatrix return.
FacetItem /stats Input to buildFacetTable{ metadata?, dieCount? }.
FacetValue /stats One distinct value — { value, waferCount, dieCount }.
FacetCuration /stats One curation entry — label, facet?, date?.
BuildFacetTableOptions /stats buildFacetTable options — curation?, facetableOnly?.

13 Current limitations

  • Ring segmentation uses equal-width radial bands. Configurable breakpoints are planned.