@wafertools/wafermap 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/AGENTS.md +6 -6
  2. package/CHANGELOG.md +212 -0
  3. package/README.md +5 -5
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +15 -10
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +7 -0
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/binCluster.d.ts +7 -1
  9. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +55 -3
  13. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/correlation.js +1 -2
  15. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +2 -0
  16. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/histogram.d.ts +16 -0
  18. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  19. package/dist/packages/canvas-adapter/charts/palette.d.ts +2 -0
  20. package/dist/packages/canvas-adapter/charts/palette.js +1 -1
  21. package/dist/packages/canvas-adapter/charts/plotChart.d.ts +27 -0
  22. package/dist/packages/canvas-adapter/charts/plotChart.js +1 -0
  23. package/dist/packages/canvas-adapter/charts/pointInteractions.d.ts +45 -0
  24. package/dist/packages/canvas-adapter/charts/pointInteractions.js +1 -0
  25. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +2 -0
  26. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  27. package/dist/packages/canvas-adapter/charts/scatter.d.ts +19 -0
  28. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  29. package/dist/packages/canvas-adapter/charts/sweep.d.ts +12 -0
  30. package/dist/packages/canvas-adapter/charts/sweep.js +1 -1
  31. package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +7 -0
  32. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  33. package/dist/packages/canvas-adapter/charts/trend.d.ts +11 -0
  34. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  35. package/dist/packages/canvas-adapter/confirmDialog.d.ts +13 -0
  36. package/dist/packages/canvas-adapter/confirmDialog.js +1 -0
  37. package/dist/packages/canvas-adapter/dataTab.d.ts +131 -0
  38. package/dist/packages/canvas-adapter/dataTab.js +1 -0
  39. package/dist/packages/canvas-adapter/dieList.d.ts +39 -4
  40. package/dist/packages/canvas-adapter/dieList.js +6 -7
  41. package/dist/packages/canvas-adapter/drilldown.d.ts +20 -3
  42. package/dist/packages/canvas-adapter/drilldown.js +1 -1
  43. package/dist/packages/canvas-adapter/guideWindow.d.ts +96 -0
  44. package/dist/packages/canvas-adapter/guideWindow.js +1 -0
  45. package/dist/packages/canvas-adapter/identityHeader.d.ts +16 -0
  46. package/dist/packages/canvas-adapter/identityHeader.js +1 -1
  47. package/dist/packages/canvas-adapter/index.d.ts +3 -3
  48. package/dist/packages/canvas-adapter/index.js +1 -1
  49. package/dist/packages/canvas-adapter/insightsTab.d.ts +73 -54
  50. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  51. package/dist/packages/canvas-adapter/maplessSummary.d.ts +2 -2
  52. package/dist/packages/canvas-adapter/plotEditor.d.ts +29 -0
  53. package/dist/packages/canvas-adapter/plotEditor.js +1 -0
  54. package/dist/packages/canvas-adapter/plotItems.d.ts +32 -0
  55. package/dist/packages/canvas-adapter/plotItems.js +1 -0
  56. package/dist/packages/canvas-adapter/plotModal.d.ts +30 -0
  57. package/dist/packages/canvas-adapter/plotModal.js +1 -0
  58. package/dist/packages/canvas-adapter/plotStore.d.ts +52 -0
  59. package/dist/packages/canvas-adapter/plotStore.js +1 -0
  60. package/dist/packages/canvas-adapter/plotTab.d.ts +30 -0
  61. package/dist/packages/canvas-adapter/plotTab.js +1 -0
  62. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +3 -2
  63. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  64. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +21 -2
  65. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  66. package/dist/packages/canvas-adapter/summaryPanel.d.ts +27 -8
  67. package/dist/packages/canvas-adapter/summaryPanel.js +2 -4
  68. package/dist/packages/canvas-adapter/summaryRail.d.ts +7 -0
  69. package/dist/packages/canvas-adapter/summaryRail.js +1 -0
  70. package/dist/packages/canvas-adapter/sweepEditor.d.ts +17 -0
  71. package/dist/packages/canvas-adapter/sweepEditor.js +1 -0
  72. package/dist/packages/canvas-adapter/tableExport.d.ts +22 -0
  73. package/dist/packages/canvas-adapter/tableExport.js +3 -0
  74. package/dist/packages/canvas-adapter/toolbar.d.ts +25 -97
  75. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  76. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  77. package/dist/packages/canvas-adapter/userGuideHtml.js +337 -42
  78. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  79. package/dist/packages/canvas-adapter/version.js +1 -1
  80. package/dist/packages/canvas-adapter/viewSwitch.d.ts +13 -0
  81. package/dist/packages/canvas-adapter/viewSwitch.js +1 -0
  82. package/dist/packages/canvas-adapter/virtualTable.d.ts +44 -0
  83. package/dist/packages/canvas-adapter/virtualTable.js +1 -0
  84. package/dist/packages/core/classify.d.ts +3 -2
  85. package/dist/packages/core/classify.js +1 -1
  86. package/dist/packages/core/passBins.d.ts +17 -4
  87. package/dist/packages/core/passBins.js +1 -1
  88. package/dist/packages/core/ringCount.d.ts +22 -0
  89. package/dist/packages/core/ringCount.js +1 -0
  90. package/dist/packages/core/tableCsv.d.ts +43 -0
  91. package/dist/packages/core/tableCsv.js +3 -0
  92. package/dist/packages/renderer/binColors.d.ts +6 -6
  93. package/dist/packages/renderer/binColors.js +1 -1
  94. package/dist/packages/renderer/buildView.d.ts +1 -1
  95. package/dist/packages/renderer/buildView.js +1 -1
  96. package/dist/packages/renderer/buildWaferMap.js +1 -1
  97. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  98. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  99. package/dist/packages/stats/binPareto.d.ts +1 -1
  100. package/dist/packages/stats/binPareto.js +1 -1
  101. package/dist/packages/stats/histogram.d.ts +27 -0
  102. package/dist/packages/stats/histogram.js +1 -1
  103. package/dist/packages/stats/index.d.ts +2 -0
  104. package/dist/packages/stats/index.js +1 -1
  105. package/dist/packages/stats/lotDrift.js +1 -1
  106. package/dist/packages/stats/lotPattern.js +1 -1
  107. package/dist/packages/stats/math.d.ts +9 -6
  108. package/dist/packages/stats/math.js +1 -1
  109. package/dist/packages/stats/patternClassification.d.ts +1 -1
  110. package/dist/packages/stats/patternClassification.js +1 -1
  111. package/dist/packages/stats/plotData.d.ts +258 -0
  112. package/dist/packages/stats/plotData.js +1 -0
  113. package/dist/packages/stats/plotId.d.ts +3 -0
  114. package/dist/packages/stats/plotId.js +1 -0
  115. package/dist/packages/stats/plotSpec.d.ts +119 -0
  116. package/dist/packages/stats/plotSpec.js +4 -0
  117. package/dist/packages/stats/presentation.d.ts +2 -0
  118. package/dist/packages/stats/presentation.js +1 -1
  119. package/dist/packages/stats/regions.d.ts +6 -0
  120. package/dist/packages/stats/regions.js +1 -1
  121. package/dist/packages/stats/renderSummaryReport.d.ts +8 -3
  122. package/dist/packages/stats/renderSummaryReport.js +15 -15
  123. package/dist/packages/stats/reportHtml.d.ts +1 -1
  124. package/dist/packages/stats/reportHtml.js +36 -29
  125. package/dist/packages/stats/scatter.d.ts +6 -0
  126. package/dist/packages/stats/scatter.js +1 -1
  127. package/dist/packages/stats/sweep.d.ts +37 -1
  128. package/dist/packages/stats/sweep.js +1 -1
  129. package/dist/packages/stats/sweepText.d.ts +14 -0
  130. package/dist/packages/stats/sweepText.js +1 -0
  131. package/dist/packages/stats/sweepXFromName.d.ts +14 -0
  132. package/dist/packages/stats/sweepXFromName.js +1 -1
  133. package/dist/packages/stats/synthesis.d.ts +14 -4
  134. package/dist/packages/stats/synthesis.js +1 -1
  135. package/dist/packages/stats/testPassRate.d.ts +6 -0
  136. package/dist/packages/stats/testPassRate.js +1 -1
  137. package/dist/packages/stats/trend.js +1 -1
  138. package/dist/packages/stats/types.d.ts +1 -1
  139. package/dist/packages/stats/yield.d.ts +15 -6
  140. package/dist/packages/stats/yield.js +1 -1
  141. package/llms.txt +15 -3
  142. package/package.json +3 -3
package/AGENTS.md CHANGED
@@ -110,20 +110,20 @@ worth reporting, not a pattern to build on.
110
110
  map that is already mounted goes through `setResult()` — do not `destroy()` and
111
111
  remount.
112
112
  - **The analysis surfaces are already built — do not reimplement them.** Pass
113
- `statsSummary` to `renderWaferMap` and it mounts the Summary panel; pass
114
- `insights: { enabled: true }` and it mounts the chart suite (yield, bin pareto,
115
- boxplot, histogram, correlation, scatter, capability, and one card per
116
- `insights.sweeps` entry). `renderWaferGallery` takes the same option across a whole
113
+ `statsSummary` to `renderWaferMap` and it mounts the Summary panel. The
114
+ chart suite (Insights) is on by default (`insights: { enabled: false }` removes it): yield, bin pareto,
115
+ boxplot, histogram, correlation, scatter, capability, and a Plot tab where the reader's own plots
116
+ and sweeps (`chart: 'sweep'`, in `insights.plots`) are cards. `renderWaferGallery` takes the same option across a whole
117
117
  lot. Supply or replace the analysis later with `setStatsSummary()`. There is no
118
118
  chart-data API to hand-build them from. Charting a selection or one wafer (right-click → histogram, capability,
119
- sweeps) is built in too, with no wiring.
119
+ saved plots and sweeps) is built in too, with no wiring.
120
120
  - **A value computed from other tests is a derived test, not a host-side column.** Pass
121
121
  `derivedTests` (a `TestDef` plus an `expression`, e.g. `'abs(t[1020] - t[1010])'`) to
122
122
  `buildWaferMap`; it then behaves as a measured test everywhere, marked `†` as not
123
123
  measured. Computing it in the host and injecting it into `testValues` loses that
124
124
  mark, the missing-input rule (absent, never 0) and the collision check. A boolean
125
125
  expression must be declared `testType: 'F'`.
126
- - **A sweep's x values are data, not guesses.** Give `xValues`, or `xFromName` (a
126
+ - **A sweep's x values are data, not guesses.** In a sweep plot's `sweep.series`, give `xValues`, or `xFromName` (a
127
127
  `{x}` placeholder pattern, not a regex) when the swept value is only in the test
128
128
  name. Never derive x from test numbers: they are identifiers, not a scale.
129
129
  - **Click-to-highlight is wired, not hand-rolled.** `onSelect` reports what the user
package/CHANGELOG.md CHANGED
@@ -22,6 +22,218 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
25
+ ## [0.34.0] — 2026-10-06
26
+
27
+ ### Added
28
+
29
+ - **The Plot tab has Delete all plots….** It removes every saved plot, sweeps included, after a confirmation that says how many and
30
+ points to **Export plots…** for a copy; **Undo** restores the whole list for ten seconds. The button is dimmed while the list is
31
+ empty. The confirmation is a small modal that starts on Cancel, closes on Escape or ✕ as Cancel, and has no Maximize button
32
+ (`OverlayOptions.maximizable: false`).
33
+
34
+ - **The right-click Value histogram offers Edit as new plot.** It closes the histogram and opens the same test as a draft histogram plot over the same dies, in the plot editor, kept only if you add it. It appears where the host keeps plots.
35
+
36
+ - **A plotted test's limits are drawn.** In a plot of a test that has limits, the axis that measures it carries them as dashed lines — short dashes for test limits, long for spec limits, each labelled with its value. They apply to a histogram's values, a scatter's X or Y, and a box, bar or line's values when those are measured values or their mean, median, minimum or maximum (not a sum, a count, a yield or a wafer figure). The axis includes them when that leaves the data a third of it, and a limit off the edge is marked there. Customise has a **Limits** choice (`limits`: `both`, `test`, `spec` or `none`). A test whose limits the wafers disagree on has none drawn.
37
+ - **A saved sweep opens in its editor from the right-click menu**, over just the selected dies, like a saved plot: it can be edited there and the edit is saved to the sweep.
38
+
39
+ - **Yield can be plotted per ring, quadrant, bin or die position.** In a bar (pooled yield: passing dies over judged dies) or a line, Yield can be split by a per-die field, and a bar of die counts per wafer can be coloured by one. Die X and die Y can be a bar's or box's categories.
40
+ - **The plot editor says what goes with what.** In the X, Y and Colour lists a field that would leave a plot that cannot be drawn is dimmed with the reason ("Yield is per wafer: use a bar or line chart"), and does nothing when picked. A plot that cannot be drawn names the problem and what to use instead. A category given to a role that needs a number (a plot saved that way) is reported, where it used to throw. `combinationIssue` is the one rule behind both the list and the chart.
41
+
42
+ - **Right-click a bin in the legend to chart or tabulate its dies.** On a map's legend, or the gallery's lot legend (also by the Menu key or Shift+F10 on a focused entry), the drilldown menu opens on the dies in that hard or soft bin — on that wafer, or on every wafer for the lot legend — without selecting them. With several bins filtered in, right-clicking one of them opens on all of them, with **Only bin N** / **All filtered bins** to switch. Lot-stack maps are unaffected.
43
+
44
+ - **Drill down on every die a finding selects.** Right-click a finding in the Summary panel to show it and open the drilldown menu on its dies, across all the wafers of a gallery. Right-clicking a selected die on a gallery card, when dies are selected on other cards too, opens the menu on all of them, with a first row (**Only this wafer** / **All selected**) to narrow it to that card's own selection. A finding about whole wafers, with no dies to select, opens the menu on those wafers.
45
+
46
+ - **The right-click menu on a selection offers New sweep….** It opens the sweep editor as a draft on the first tests the selected dies hold, drawn over just those dies, and keeps the sweep only if the reader adds it, as New plot… does.
47
+
48
+ - **A sweep remembers its tests' names.** `series[].testNames` records what each test was called when the sweep was set up (the sweep editor and **+ New sweep** fill it in). A test whose name now differs is not drawn and the card names it, and the crossing and widths are not measured, so a sweep opened on a lot from another test program that reuses the numbers does not draw another measurement as if it were the original. A sweep without it is not checked.
49
+
50
+ - **A Data tab in Insights.** The last Insights sub-tab shows the scope as tables, one at a time: **Statistics** (the
51
+ test-values and functional-tests tables, one set per group under Group by), **Dies** (one row per die, a column per
52
+ test, sortable, drawn as a virtual table so a lot of hundreds of thousands of dies scrolls smoothly) and **Wafers**
53
+ (one row per wafer: metadata, die counts, yield, and the mean of the first 50 tests). Each has **Export CSV**,
54
+ which writes the table as shown in the order shown, and **Copy** (tab-separated, up to 200,000 cells). The Dies
55
+ export can be **Wide** (a column per test) or **Long** (a row per die per test). `InsightsOptions.defaultView`
56
+ accepts `'data'`, and the tab button carries `data-wmap-insights-tab="data"`.
57
+ - **The docked Summary panel has no CSV buttons.** The "Test values CSV" and "Functional CSV" buttons are gone from its
58
+ tables (and from the lot panel's); the **Data tables** button beside the report button opens the same tables, with Export CSV
59
+ and Copy. Insights' Data tab and the modal keep them.
60
+ - **The Summary panel's "View die list" is now "Data tables".** It opens the Data tab's tables in a modal over the wafer or lot the
61
+ panel describes — Statistics and Dies, plus Wafers for a lot — each with Export CSV and Copy, and it works with Insights
62
+ turned off. The Dies table is virtual, so `dieList.maxRows` no longer limits it (it still limits the coordinate-less map
63
+ replacement and the "+N dies without position" footer). `dieList.enabled` gates the button as before.
64
+ - **A die row in a table shows that die on the map.** In the Dies table (the Data tab, a drilldown selection, the Summary panel's
65
+ Data tables), clicking a row steps the table aside (or leaves Insights) and rings that die on its map, on its own card in a
66
+ gallery, scrolled into view. The ring is not a selection: nothing else fades, and it goes with the next click on the map or Esc.
67
+ Rows are clickable only where the host has a map to show.
68
+ - **A gallery can pick whole wafers.** Ctrl/Cmd+click a card's header to add or remove its wafer (outlined, counted in the
69
+ toolbar, cleared by clicking the count). Right-clicking a picked card opens the drilldown menu on every die of every picked
70
+ wafer, with a Wafers table; dies selected on a map take precedence.
71
+ - **A gallery can select the same dies on every wafer.** A new toolbar toggle, **Select on every wafer** (off by default),
72
+ applies a box, a click or a clear made on any card at the same die positions on every card, so a region (the edge ring, a
73
+ scratch zone, a reticle corner) is read lot-wide. Right-click, the Menu key or the Chart button on any card or its header
74
+ then opens the drilldown menu on those dies across all the wafers: the charts, plus Dies, Test statistics and Wafers
75
+ ("selected at the same die positions on N wafers"), saved under the gallery's own file names. There is no option for it:
76
+ it is a toolbar toggle only. The button carries a count of the selected die positions; clicking it with a selection
77
+ showing clears the selection and stays on, and turning it off clears the selection on every card.
78
+ - **The Insights scatter is interactive.** Hovering a point names its wafer and die and its two values; clicking it opens that wafer
79
+ on the X test (a single-wafer host shows the test on its map); dragging a rectangle selects the dies inside it, across any
80
+ number of wafers, ringed until the X/Y test changes or empty space is clicked, and opens the drilldown menu for them —
81
+ the histogram, capability and sweep charts, and the Dies, Test statistics and, for more than one wafer, Wafers tables.
82
+ Scatter points now carry their `die` and `waferIndex` (`ScatterPoint`, `ScatterItem`).
83
+ - **Drilldown can open the selection as a table.** The right-click menu on a map selection, a wafer or a chart mark gains
84
+ **Dies** and **Test statistics**: the Data tab's tables over just those dies, in a modal that names the population,
85
+ with Export CSV and Copy (a saved file is named for the selection and carries a Wafer column). On a bins-only
86
+ map the charts and Test statistics are greyed with the reason.
87
+
88
+ - **A Plot tab in Insights: build, keep and reload your own plots.** The last Insights sub-tab is a chart builder. **+ New
89
+ plot** opens a plot beside a large copy of its chart; choose the chart type (scatter, histogram, box, bar or line), the
90
+ field for X, Y and colour (any parametric test, a die's X or Y, a wafer's yield or die count, ring, quadrant, hard or soft bin,
91
+ site, or a wafer or lot field such as split, slot or temperature), and each axis's title, limits, log scale and
92
+ direction. The field lists are grouped and filter by name or test number. Titles are automatic until typed in.
93
+ Colour follows the Group by control unless the plot names its own field, and a scatter can be coloured on a gradient by
94
+ a measured value or a wafer figure. A bar of Yield is pooled (passing dies over judged dies) rather than an average of
95
+ percentages, a plot can draw one mark per wafer with its dies' values combined by mean, median, minimum, maximum, sum
96
+ or count, and a per-wafer figure is never split by a per-die field (it is reported instead). Every plot states its wafers and dies, how
97
+ values were combined, and how many dies were left out for a missing value. Points hover, click to open their wafer,
98
+ and drag to select dies into the right-click menu. A plot is a recipe with no population, so it is kept for the next
99
+ lot: `insights.plots` supplies the list, `insights.onPlotsChange` reports it after every change (a burst of typing is
100
+ one call), and **Export plots…** / **Import plots…** carry it as a `wafermap-plots` file (`onPickPlotsFile` for a
101
+ native dialog). A plot that needs a test the open lot lacks is kept and greyed with the reason, and a test is matched
102
+ by number and checked by name, so a plot saved against another test program is reported rather than drawn against the
103
+ wrong measurement. The Plot tab is loaded the first time it is opened.
104
+ - **Add examples, and titles that say when they are out of date.** The Plot tab's **Add examples** draws one plot of each
105
+ chart type the lot can show (scatter of the first two tests, histogram, box by wafer, bar of yield by the first lot field
106
+ that divides the wafers, line over wafer order, and a sweep of the first tests in test order) and adds only those not
107
+ already there. A title the reader typed is checked
108
+ against its plot: the card and the editor say when it names a test the plot no longer shows, or names one of its two
109
+ fields but not the other, and offer the automatic title. Copying a plot numbers its copies ("Vth (copy 2)") instead of
110
+ stacking marks, and a copy of an untitled plot stays untitled so its title keeps following its fields.
111
+ - **Clicks on Insights charts open what the mark counts.** A plot's point, bar or box opens its wafer on the plot's own test
112
+ (it opened the bin map before), or shows the test on the map in a single-wafer host. The bin pareto (and the grouped
113
+ one's sub-bars), a pass-rate row (the dies failing that test, judged as the card is), a bar of the value histogram, a
114
+ ring or quadrant of the yield diagrams, and a plot's histogram bar, non-wafer bar or box, and line point each open the
115
+ right-click menu on exactly the dies they count; a Wafers-table row opens its wafer. `dieFailsTest`, `diesInBucket`,
116
+ `diesInRegion` and `bucketIndexOf` are the rules those picks share with the tallies they match.
117
+ - **More of Insights is clickable, and a chart with many groups keeps its colours apart.** With Group by on, a column of the
118
+ overlaid value histogram opens the menu on that bucket's dies (across the groups, or the one the legend has emphasised);
119
+ a level of a sweep curve opens the dies measured there; a drag across the wafer-to-wafer trend selects the wafers it
120
+ crosses, ringed, and opens the menu on their dies (a click on a point still opens that wafer). The categorical series
121
+ palette has sixteen colours instead of eight (the first eight are unchanged): the second round was chosen for distance
122
+ from the first in CIELAB and for staying distinct under deuteranopia and protanopia, so a lot of 13 wafers coloured by
123
+ wafer no longer gives two wafers the same colour. `categorical(i)` wraps past sixteen. `wirePointInteractions` gains
124
+ `locate`, for charts whose marks are columns.
125
+ - **Saved plots in the right-click menu.** The drilldown menu has a **Plots** section: each saved plot is drawn over the
126
+ selected dies, and **New plot…** starts a draft on the selection that is kept only if it is added.
127
+ - **`readPlotsFile` and `writePlotsFile`** (and the `PlotSpec` type) read and write the plots file. The reader is
128
+ lenient (it keeps every readable plot and names each dropped setting), keeps settings a newer version wrote, and
129
+ keeps a plot whose chart type it does not know.
130
+ - **Sweeps are plots, with an editor.** A sweep is a plot with `chart: 'sweep'`: a card on the Plot tab beside the reader's
131
+ other plots, with Edit, Duplicate and Delete (and Undo), kept, exported and imported like any plot, and a row in the
132
+ right-click menu's Plots section over the selected dies. A level of its curve opens the dies measured there, as before.
133
+ **+ New sweep** starts one on the lot's first tests, and **Edit** opens a sweep editor beside a live copy of the curve:
134
+ the title, a block per series (its tests in sweep order, typed as numbers and ranges such as `1010, 1011, 1020..1030`,
135
+ and whether X is the test order, typed values or read from the test names), the axis title, unit and scale, whether the
136
+ first two series' crossing is marked, and the Y levels at which their width is measured. Text that is not a test number
137
+ or a range is flagged where it is typed and not applied. A sweep whose tests are not in the open lot is kept and greyed,
138
+ and draws again when a lot with its tests is opened. `PlotSpec` gains the optional `sweep` (a `SweepSpec` without its
139
+ `id` and `title`), and `readPlotsFile` also reads a `tsmap-sweeps` file, turning each sweep into a sweep plot under
140
+ the same `id`. When a name pattern (`xFromName`) finds no X in a test's name, the warning on the chart shows the first
141
+ three of those names, as they are in the data, so the pattern can be written against them. The sweep editor shows the same: under a name pattern it lists the first four
142
+ test names and what the pattern reads from each as it is typed, with a folded list of worked **Pattern examples**
143
+ (`@ {x}` on `Fmax @ 0.55 V`, `-{x}` on `1234-5`), which a test keeps true against the matcher. A name pattern's `?` matches exactly one character
144
+ (`123?{x}` reads 4 from `12314`), alongside `*` for any run of text.
145
+ - **A plot's JSON uses `chart` and `fields`.** A plot names its kind of chart in `chart` (`scatter`, `histogram`, `box`, `bar`,
146
+ `line` or `sweep`) and the field for each role in `fields` (`x`, `y`, `color`); a sweep has no `fields`.
147
+ - **Reset and Cancel in the plot and sweep editors.** Edits are kept as they are made, so the editor's footer, which stays in
148
+ view while the settings scroll, has **Reset** (back to the plot as it was when the window opened; disabled until something
149
+ changed) and **Cancel** (the same, then close; a draft is discarded). Closing with the window's own button keeps the changes.
150
+
151
+ ### Breaking
152
+
153
+ - **Pass bins have one default, applied once.** Bin 1 passes when an input states no pass bins, and `buildWaferMap` applies that
154
+ default and nothing else does. Every view, report, chart and table takes the pass bins of the map it describes and refuses to
155
+ state a yield without them: `renderSummaryReport`'s `passBins`, `buildView`'s `passBins` option and each item of a lot report are
156
+ required unless the item is a built map, which carries its own. A chart or table opened on a selection, a wafer, a gallery card or
157
+ a saved plot judges by that wafer's own pass bins, so a lot built with pass bins 3 and 5 reports the same yield everywhere
158
+ it is shown. The gallery's stacked modes judge by the pass bins and ring count the wafers share and are not offered for wafers
159
+ that differ. The ring count follows the same rule: `buildWaferMap` applies its default (4) and every ring, quadrant and ring
160
+ figure reads the map's own, so `renderSummaryReport`'s `ringCount` and `buildView`'s `ringCount` option (when rings are drawn)
161
+ are required unless the map is a built one.
162
+
163
+ - **The Maps | Insights switch is at the far right of the toolbar row, and the toolbar has no Insights button.** The switch sits after the toolbar and stays in the same place in both views (alone at the right edge when there is no toolbar), so it is both the way into Insights and the way back. The toolbar's Insights button and its `data-wmap-insights-btn` hook are gone: a host that clicked the hook should click `[data-wmap-view="insights"]` (or `[data-wmap-view="maps"]`), whose `aria-selected` says which view is showing. `setInsightsOpen` and `insights.enabled` are unchanged.
164
+
165
+ - **Right-click on a map, a gallery card or a chart mark always opens the drilldown menu.** The menu always has **Dies**, and
166
+ lists charts and plots where there is something to draw (a parametric test or a saved plot), so a host's own context menu
167
+ on those surfaces does not appear. The toolbar's Chart button is always shown and is labelled "Charts and tables for …".
168
+ - **`onSaveText` can receive a `Blob`.** `SaveTextHandler`'s first argument is now `string | Blob`. A table of a million
169
+ cells or more (a lot's die list, 400k dies × 50 tests being 20 million) is written in slices and handed over as a
170
+ `Blob`, so no single string holds the whole file. Smaller tables are still a string, saved before the call returns.
171
+ A host that writes the text to a file should write `blob.stream()` for a `Blob`; one that reads it uses
172
+ `await blob.text()`.
173
+ - **Insights is on by default.** `insights.enabled` now defaults to `true` wherever there is a toolbar: always
174
+ for `renderWaferGallery`, and for `renderWaferMap` unless `showToolbar` is `false` (a chromeless map stays a
175
+ plain map). Hosts that do not want it pass `insights: { enabled: false }`. Gallery cards are never Insights
176
+ hosts. The chart code still downloads only when Insights is first opened.
177
+
178
+ ### Changed
179
+
180
+ - **Sweeps are drawn on the Plot tab; the Sweeps tab is gone.** Each sweep is a card there, in the same list as the other
181
+ plots. `insights.sweeps` and `insights.onRemoveSweeps` are deprecated and still work: each entry is drawn as a sweep plot
182
+ under its own `id` (a plot already saved under that `id` takes precedence, so a host that keeps both shows it once), the
183
+ first use logs a notice, and deleting one on the Plot tab calls `onRemoveSweeps` so the host stops supplying it.
184
+ `defaultView: 'sweeps'` opens the Plot tab. The right-click menu lists sweeps with the plots, in its Plots section.
185
+
186
+ - **A chart saved as a PNG says what it is, and a printed chart says the same.** Every Insights chart card now reads back
187
+ what it shows around its canvas: its title, what its controls are set to (`Test: Vth`, `Wafer: W03`, `X: Vth · Y: Idsat`,
188
+ ticked toggles), the lines it states about itself (population and N, `box = Q1–Q3, …`, the normalisation of the capability
189
+ chart, the correlation sample note), and its colour key. A saved image puts these in a header above the chart, with the
190
+ instructions to click or drag removed; the camera button's file is named for the title as it reads then. A chart whose key
191
+ is a gradient (a coloured scatter, the correlation matrix) gets a colour bar with its range. The image takes the card's own
192
+ background instead of white, so a dark theme saves a legible dark chart. In print, the controls and on-screen hints are
193
+ hidden (`data-wmap-noprint`, one `@media print` rule) and each card shows a block, refilled just before printing, giving the
194
+ same settings and lines. Panels mark the text that belongs in them with `data-wmap-caption`. `makeWaferSelect`'s accessible
195
+ name is now "Wafer" (it was its "All wafers" label). `saveCanvasPng` gains an optional header and is otherwise unchanged.
196
+ - **Controls that act on the screen are left off a printed page**: the camera and expand buttons on chart cards, and the Plot
197
+ tab's New plot, Add examples, Import, Export, Edit, Duplicate and Delete with their notices.
198
+ - **The Overview no longer carries the test-values and functional-tests tables.** They moved, unchanged, to the Data
199
+ tab's Statistics view; the Overview ends with a link to it. Hosts that read the "Test values CSV" or "Functional
200
+ CSV" button from the Overview should open the Data tab first. The Summary panel's own tables are unchanged.
201
+ - **Large CSV exports stay responsive and use less memory.** A big table is built in slices of about 30 ms, with
202
+ progress on the die list's Export CSV button, instead of freezing the page. At 400k dies × 50 tests (201 MB) the
203
+ export completes within 250 MB of heap where a single string needs about 400 MB, and it is no longer limited by the
204
+ engine's maximum string length.
205
+ - **CSV exports carry full-precision numbers.** The test-values, functional-test, die-list and correlation exports write
206
+ every measured value and statistic as a plain number (`452.123456789`, `1.5e-7`) instead of the screen's four-figure
207
+ formatting (`452.1`, `150E-9`). A value from a single-precision tester field is written as the shortest decimal that
208
+ is still that value (`0.5123457`, not `0.51234567165374756`); anything else keeps up to 15 significant digits. Percentages keep one decimal, Ppk three, and
209
+ correlation r six. A non-finite value is an empty cell. All four exports are written by one function
210
+ (`core/tableCsv.ts`), so quoting and number format are the same in each.
211
+ - **The metadata fields strip fills the width it has.** On the gallery legend and the Insights strip, fields are taken in
212
+ priority order and each lists as many of its values as fit (up to 12), instead of three; a field that cannot fit even
213
+ one value, and those after it, move behind `N more fields`, so the button appears only when something is hidden. The
214
+ strip re-fits when its width changes. The Data tab's Wide | Long choice is labelled **Export format**.
215
+ - **The identity strip has one overflow control.** A field with many distinct values shows its count in the
216
+ label and an ellipsis (`Lot (5): LOT-A, LOT-B, LOT-C, …`); the only button is `N more fields`, which
217
+ reveals the remaining metadata fields.
218
+ - **A Map | Insights switch leads the chrome row.** With `insights.enabled`, the first control in the row
219
+ above the map (single map: Map | Insights; gallery: Maps | Insights) is a labelled two-tab switch that stays
220
+ in the same place in both views, so it works as a toggle and makes Insights easy to find. It replaces the
221
+ back tab at the start of the Insights tab row, which now holds only Overview · Distributions · Correlation.
222
+ The toolbar's Insights button is unchanged. Hosts waiting for the Insights chunk should poll
223
+ `button[data-wmap-insights-tab]`, not `button[role="tab"]`.
224
+ - **A labelled "Summary" tab opens the Summary panel while it is closed.** A slim vertical tab on the outer
225
+ edge of the map area (single map and gallery, left or right placement) shows only while the panel is closed
226
+ and a Summary exists, tints like the toolbar icon when there are notable findings, and opens the panel. The
227
+ toolbar's Summary button is unchanged.
228
+ - **"What stands out" gives the top item the emphasis.** The largest item keeps its tinted box and full
229
+ sentence; the others are one compact list (impact marker, the item in a few words, share of the dies),
230
+ in the panel and the report alike. The summary's `also` line is replaced by that list.
231
+ - **One σ rule: the sample standard deviation (n−1).** The Test Values table, per-test statistics, the
232
+ lot trend, the lot drift σ and the report's pooled σ all divide by n−1, as the capability indices
233
+ already did, so a cell shows the same σ whichever path filled it. A displayed σ is √(n/(n−1)) times
234
+ the population σ of the same values (about 2.5% at n = 20; negligible on large lots). Exported
235
+ `sampleVariance` in `stats/math.ts` is the single implementation.
236
+
25
237
  ## [0.33.0] — 2026-10-02
26
238
 
27
239
  ### Breaking
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![CI and deploy](https://github.com/wafertools/wafermap/actions/workflows/deploy.yml/badge.svg)](https://github.com/wafertools/wafermap/actions/workflows/deploy.yml)
6
6
  [![npm](https://img.shields.io/npm/v/@wafertools/wafermap.svg)](https://www.npmjs.com/package/@wafertools/wafermap)
7
7
  ![runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)
8
- ![bundle](https://img.shields.io/badge/data%20layer%20min%2Bgz-~60%20kB-blue)
8
+ ![bundle](https://img.shields.io/badge/data%20layer%20min%2Bgz-~64%20kB-blue)
9
9
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
10
10
 
11
11
  <img src="docs/images/hero-test-values.png" alt="wafermap demo" style="max-width:640px; display:block; margin:8px 0;" />
@@ -13,7 +13,7 @@
13
13
  Browser-first wafer map visualization for semiconductor test data.
14
14
 
15
15
  **Zero runtime dependencies.** Pure ES modules with TypeScript types — works in React,
16
- Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~60 kB
16
+ Svelte, Vue, plain HTML, or a Web Worker. The DOM-free data-and-stats layer is ~64 kB
17
17
  min+gz; the interactive renderer is larger, and its chart suite and in-app guide are
18
18
  loaded on demand rather than shipped up front — [measured sizes](docs/performance.md).
19
19
 
@@ -31,9 +31,9 @@ wafermap renders interactive wafer maps from semiconductor prober output. Hard b
31
31
  - Geometry inference — pass full physical dimensions or raw prober step positions; die pitch, wafer diameter, and coordinate origin are resolved automatically
32
32
  - `renderWaferMap` — interactive canvas map with toolbar, zoom/pan, tooltips, die selection, and summary panel
33
33
  - `renderWaferGallery` — lot-level card grid with shared controls and click-to-expand
34
- - Insights tab (`insights: { enabled: true }`) — an in-toolbar chart suite (yield, per-test pass rate, bin pareto, capability, boxplot, histogram, wafer-to-wafer trend, correlation, scatter) computed from the same wafer/lot data, for one wafer or the whole gallery
34
+ - Insights tab (`insights: { enabled: true }`) — a chart suite behind the Maps | Insights switch (yield, per-test pass rate, bin pareto, capability, boxplot, histogram, wafer-to-wafer trend, correlation, scatter) computed from the same wafer/lot data, for one wafer or the whole gallery
35
35
  - Derived tests (`derivedTests`) — tests computed per die from other tests (`abs(t[1020] - t[1010])`), which then work everywhere a measured test does, marked † as not measured
36
- - Parametric sweeps (`insights.sweeps`) — an ordered run of tests read as a response curve, with the crossing and widths between two curves measured; x values given directly or read from the test names, on a linear or log axis
36
+ - Parametric sweeps (plots with `chart: 'sweep'`, edited on the Plot tab) — an ordered run of tests read as a response curve, with the crossing and widths between two curves measured; x values given directly or read from the test names, on a linear or log axis
37
37
  - Drilldown — right-click selected dies, a wafer card or a wafer's bar to chart just that population, with the population stated on the chart
38
38
  - `analyzeWaferMap` / `analyzeWaferLot` — spatial analysis across rings, quadrants, sectors, and reticle positions; failure cluster detection; lot trend series
39
39
  - Pure ES modules, no server, no runtime dependencies — works in React, Svelte, Vue, plain HTML, or a Web Worker
@@ -75,7 +75,7 @@ export. Add `analyzeWaferMap` for the findings and summary panel, swap in
75
75
 
76
76
  The API reference is long because it documents every option, not because you need
77
77
  them: [tsmap](https://github.com/wafertools/tsmap), a complete cross-platform desktop
78
- application built on this library, imports **16** of its ~100 exports. Read the
78
+ application built on this library, imports **18** of its ~100 exports. Read the
79
79
  [Quick Start](https://wafertools.github.io/wafermap/quickstart/) first and treat the
80
80
  [API reference](https://wafertools.github.io/wafermap/api/) as something to search,
81
81
  not to read.
@@ -1,12 +1,19 @@
1
1
  import type { Die } from '../core/dies.js';
2
- import { type TestDef } from '../renderer/buildWaferMap.js';
3
- import type { SweepSpec } from '../stats/sweep.js';
2
+ import type { Wafer } from '../core/wafer.js';
3
+ import type { TestDef } from '../renderer/buildWaferMap.js';
4
4
  /** One wafer's share of a population. */
5
5
  export interface DrilldownItem {
6
6
  /** The wafer's display label. */
7
7
  label: string;
8
8
  dies: Die[];
9
9
  waferIndex?: number;
10
+ /** The wafer's geometry and metadata, when the gesture knew it — a table of these dies
11
+ * uses it for ring/quadrant and for the lot and product columns. */
12
+ wafer?: Wafer;
13
+ /** This wafer's own pass bins (the result's, never a default): every yield a chart or table of these dies states judges by them. */
14
+ passBins: readonly number[];
15
+ /** This wafer's own ring count (the result's, never a default): a ring or quadrant of these dies is the map's ring. */
16
+ ringCount: number;
10
17
  }
11
18
  /** A population a chart can be opened on. */
12
19
  export interface DrilldownSource {
@@ -23,14 +30,7 @@ export interface DrilldownSource {
23
30
  * aggregates): every chart is then unavailable, with this as the reason. */
24
31
  notMeasuredReason?: string;
25
32
  }
26
- /**
27
- * Whether any drilldown chart could exist for this data — decides whether a
28
- * right-click is taken over at all, without loading the drilldown chunk. The
29
- * distribution charts need a parametric test, a sweep needs to be defined;
30
- * with neither (a bins-only map) right-click stays the browser's or host's.
31
- * `targetsFor` in drilldown.ts is the list this summarises.
32
- */
33
- export declare function hasDrilldownTargets(testDefs: readonly TestDef[] | undefined, sweeps: readonly SweepSpec[] | undefined): boolean;
33
+ export declare const LOT_STACK_REASON = "This map stacks a lot: its dies are per-position aggregates, not measured dies";
34
34
  interface WaferFacts {
35
35
  /** The wafer's real identity, if it has one — never a positional stand-in,
36
36
  * which would read as an ID ("Wafer 3 (no ID)") in a chart title. */
@@ -39,6 +39,11 @@ interface WaferFacts {
39
39
  isLotStack?: boolean;
40
40
  activeTest?: number;
41
41
  waferIndex?: number;
42
+ wafer?: Wafer;
43
+ /** The wafer's pass bins, from its built result. */
44
+ passBins: readonly number[];
45
+ /** The wafer's ring count, from its built result. */
46
+ ringCount: number;
42
47
  }
43
48
  /** Dies the user selected on one wafer's map. */
44
49
  export declare function selectionPopulation(selected: Die[], f: WaferFacts): DrilldownSource;
@@ -1 +1 @@
1
- import{isParametricTest as s}from"../renderer/buildWaferMap.js";export function hasDrilldownTargets(t,e){return(e?.length??0)>0||(t??[]).some(s)}const n="This map stacks a lot: its dies are per-position aggregates, not measured dies";function r(t,e,a){return{items:[{label:a.waferLabel??"this wafer",dies:t,waferIndex:a.waferIndex}],population:e,testDefs:a.testDefs,activeTest:a.activeTest,notMeasuredReason:a.isLotStack?n:void 0}}export function selectionPopulation(t,e){return r(t,e.waferLabel?`selected on ${e.waferLabel}`:"selected",e)}export function waferPopulation(t,e){return r(t,e.waferLabel?`on ${e.waferLabel}`:"on this wafer",e)}
1
+ export const LOT_STACK_REASON="This map stacks a lot: its dies are per-position aggregates, not measured dies";function s(a,e,t){return{items:[{label:t.waferLabel??"this wafer",dies:a,waferIndex:t.waferIndex,wafer:t.wafer,passBins:t.passBins,ringCount:t.ringCount}],population:e,testDefs:t.testDefs,activeTest:t.activeTest,notMeasuredReason:t.isLotStack?LOT_STACK_REASON:void 0}}export function selectionPopulation(a,e){return s(a,e.waferLabel?`selected on ${e.waferLabel}`:"selected",e)}export function waferPopulation(a,e){return s(a,e.waferLabel?`on ${e.waferLabel}`:"on this wafer",e)}
@@ -59,6 +59,13 @@ export interface ChartPanel {
59
59
  * instead, via `drill.onOpenGroup` — this is never called for those.
60
60
  */
61
61
  onOpen?: (datum: ChartDatum) => void;
62
+ /**
63
+ * Clicking a bar picks out the dies it counts (a bin's dies, in the bin pareto): called with the bar and the click, so
64
+ * the host can open a menu there. Takes the place of `onOpen` for panels whose bars are not wafers. `selectLabel` is
65
+ * what the click does, in the reader's words ("chart or tabulate these dies").
66
+ */
67
+ onSelectBar?: (datum: ChartDatum, e: MouseEvent) => void;
68
+ selectLabel?: string;
62
69
  /** Right-click on a leaf row that carries a wafer (`datum.key`) — see
63
70
  * `WaferContextMenuHandler`. Not called for a group row. */
64
71
  onWaferContextMenu?: WaferContextMenuHandler;
@@ -1 +1 @@
1
- import{SPACE as nt,fontPx as ot,FONT as lt,CLR as it}from"../toolbar.js";import{cardShell as rt,formatValue as st,observeResize as ct,makeTooltip as at,positionChartTooltip as ft,makeBackButton as ut,makeSegmented as dt,fitRowsHeight as ht,setChartGrow as mt,resolveChartCanvasColors as xt,PADDING as u,VALUE_WIDTH as j,WAFER_MENU_HINT as bt,prepareCanvas as pt}from"./chartShell.js";import{escHtml as X}from"../../core/utils.js";const d=24,m=5,gt=15,z=110,yt=12;export function renderBarPanel(l,U){const{barColor:G,valueLabel:I}=l;let g=l.title,i=l.data;const{card:r,heading:E,controlsRow:P,body:S}=rt(g,U,l.ownerDocument);if(mt(r,"rows"),l.selfControl){const e=l.selfControl;P.appendChild(dt(e.options,e.current,n=>q(n),r.ownerDocument))}const{drill:f}=l;let y=!1,R=null;const A=r.ownerDocument.createElement("div");Object.assign(A.style,{color:it.label,fontSize:lt.body,marginBottom:nt.sm}),r.insertBefore(A,S);function B(){const e=[];l.onOpen&&e.push("click to open this wafer"),f&&!y&&e.push(`click a ${f.groupLabelText} to see it by wafer`);const n=e.join(", or ");let o=n?`${n[0].toUpperCase()}${n.slice(1)}.`:"";const t=l.reference?.(i);t&&(o=o?`${o} Dashed line: ${t.label}.`:`Dashed line: ${t.label}.`),A.textContent=o}B();const k=r.ownerDocument.createElement("div"),v=()=>l.reference?gt:0,Y=()=>u*2+v()+Math.min(i.length,yt)*(d+m);Object.assign(k.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${Y()}px`,scrollbarGutter:"stable"}),S.appendChild(k);const s=r.ownerDocument.createElement("canvas");s.style.display="block",s.style.cursor="default",k.appendChild(s);const w=at(r);let x=-1,M=Math.max(1,...i.map(e=>e.value));const N=e=>I?I(e):`${st(e.value)} (${e.percent.toFixed(1)}%)`;function F(e){const n=u+v()+e*(d+m),o=u+z,t=s.clientWidth-o-j-u;return{y:n,barX:o,barMaxWidth:Math.max(10,t)}}function C(){const e=u*2+v()+i.length*(d+m);k.style.maxHeight=`${ht(r,S,Y(),e)}px`;const n=k.clientWidth,o=pt(s,r,n,e);if(!o)return;const{ctx:t}=o;t.font=`${ot(-1)}px system-ui, sans-serif`,t.textBaseline="middle";const c=xt(r);i.forEach((a,p)=>{const{y:h,barX:W,barMaxWidth:H}=F(p);p===x&&(t.fillStyle=c.bgHover,t.fillRect(0,h-m/2,n,d+m)),t.fillStyle=c.text,t.textAlign="right";const T=a.label.length>16?`${a.label.slice(0,15)}\u2026`:a.label;t.fillText(T,u+z-8,h+d/2),t.fillStyle=c.track,t.fillRect(W,h,H,d);const $=Math.max(1,a.value/M*H);t.fillStyle=G?G(a,p):c.text,t.fillRect(W,h,$,d),t.fillStyle=c.text,t.textAlign="right",t.fillText(N(a),W+H+j,h+d/2)});const b=l.reference?.(i);if(b&&i.length){const{barX:a,barMaxWidth:p}=F(0),h=Math.round(a+Math.max(0,Math.min(1,b.value/M))*p)+.5,W=u+v()-3,H=u+v()+i.length*(d+m)-m,T=(Z,tt,et)=>{t.save(),t.strokeStyle=Z,t.lineWidth=tt,t.setLineDash(et),t.beginPath(),t.moveTo(h,W),t.lineTo(h,H),t.stroke(),t.restore()};T(c.bg,3.5,[]),T(c.text,1.5,[4,3]),t.save(),t.font="600 10px system-ui, sans-serif",t.textBaseline="middle",t.textAlign="left";const $=4,O=t.measureText(b.label).width+$*2,_=13,V=u+v()-_-1;let D=h-O/2;D=Math.max(a,Math.min(D,a+p-O)),t.fillStyle=c.bg,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.rect(D,V,O,_),t.fill(),t.stroke(),t.fillStyle=c.text,t.fillText(b.label,D+$,V+_/2+.5),t.restore()}}function q(e){if(!l.selfControl)return;l.selfControl.current=e;const n=l.selfControl.onChange(e);i=n.data,M=Math.max(1,...i.map(o=>o.value)),x=-1,n.title&&(g=n.title,E.textContent=g),B(),C()}function J(e){if(!f)return;const n=f.onOpenGroup(e);i=n.data,g=n.title,E.textContent=g,M=Math.max(1,...i.map(o=>o.value)),x=-1,y||(y=!0,R=ut(()=>K(),r.ownerDocument),P.appendChild(R)),B(),C()}function K(){if(!f)return;const e=f.onBack();i=e.data,g=e.title,E.textContent=g,M=Math.max(1,...i.map(n=>n.value)),x=-1,y=!1,R?.remove(),R=null,B(),C()}function L(e){const n=Math.floor((e-u+m/2)/(d+m));return n>=0&&n<i.length?n:-1}s.addEventListener("mousemove",e=>{const n=s.getBoundingClientRect(),o=L(e.clientY-n.top),t=o>=0&&f&&!y&&i[o].itemCount>1,c=o>=0&&(t||!!l.onOpen&&o>=0&&!t);if(o!==x&&(x=o,s.style.cursor=c?"pointer":"default",C()),o>=0){const b=i[o],a=t?`<br><em>click to see this ${X(f.groupLabelText)} by wafer</em>`:l.onOpen?"<br><em>click to open this wafer</em>":"",p=!t&&l.onWaferContextMenu&&typeof b.key=="number"?bt:"";w.innerHTML=`<strong>${X(b.label)}</strong><br>${X(N(b))}${a}${p}`,w.style.display="block",ft(w,r,e.clientX,e.clientY)}else w.style.display="none"}),s.addEventListener("mouseleave",()=>{x!==-1&&(x=-1,C()),w.style.display="none"}),s.addEventListener("click",e=>{const n=s.getBoundingClientRect(),o=L(e.clientY-n.top);if(o===-1)return;const t=i[o];if(f&&!y&&t.itemCount>1){J(t);return}l.onOpen?.(t)}),s.addEventListener("contextmenu",e=>{const n=s.getBoundingClientRect(),o=L(e.clientY-n.top);if(o===-1||!l.onWaferContextMenu)return;const t=i[o];f&&!y&&t.itemCount>1||typeof t.key!="number"||(w.style.display="none",l.onWaferContextMenu(t.key,void 0,e))});const Q=ct(r,()=>C());return C(),{card:r,destroy:()=>Q.disconnect()}}
1
+ import{SPACE as nt,fontPx as ot,FONT as lt,CLR as it}from"../toolbar.js";import{cardShell as rt,formatValue as st,observeResize as ct,makeTooltip as at,positionChartTooltip as ft,makeBackButton as ut,makeSegmented as dt,fitRowsHeight as ht,setChartGrow as mt,resolveChartCanvasColors as xt,PADDING as u,VALUE_WIDTH as j,WAFER_MENU_HINT as bt,prepareCanvas as pt}from"./chartShell.js";import{escHtml as D}from"../../core/utils.js";const d=24,m=5,yt=15,z=110,gt=12;export function renderBarPanel(l,U){const{barColor:G,valueLabel:I}=l;let y=l.title,i=l.data;const{card:r,heading:E,controlsRow:P,body:A}=rt(y,U,l.ownerDocument);if(mt(r,"rows"),l.selfControl){const e=l.selfControl;P.appendChild(dt(e.options,e.current,n=>q(n),r.ownerDocument))}const{drill:f}=l;let g=!1,W=null;const H=r.ownerDocument.createElement("div");H.dataset.wmapCaption="1",Object.assign(H.style,{color:it.label,fontSize:lt.body,marginBottom:nt.sm}),r.insertBefore(H,A);function $(){const e=[];l.onOpen&&e.push("click to open this wafer"),l.onSelectBar&&e.push(`click a bar to ${l.selectLabel??"see what it counts"}`),f&&!g&&e.push(`click a ${f.groupLabelText} to see it by wafer`);const n=e.join(", or ");let o=n?`${n[0].toUpperCase()}${n.slice(1)}.`:"";const t=l.reference?.(i);t&&(o=o?`${o} Dashed line: ${t.label}.`:`Dashed line: ${t.label}.`),H.textContent=o}$();const k=r.ownerDocument.createElement("div"),w=()=>l.reference?yt:0,Y=()=>u*2+w()+Math.min(i.length,gt)*(d+m);Object.assign(k.style,{overflowX:"hidden",overflowY:"auto",minHeight:"0",flex:"1",maxHeight:`${Y()}px`,scrollbarGutter:"stable"}),A.appendChild(k);const s=r.ownerDocument.createElement("canvas");s.style.display="block",s.style.cursor="default",k.appendChild(s);const C=at(r);let x=-1,M=Math.max(1,...i.map(e=>e.value));const N=e=>I?I(e):`${st(e.value)} (${e.percent.toFixed(1)}%)`;function F(e){const n=u+w()+e*(d+m),o=u+z,t=s.clientWidth-o-j-u;return{y:n,barX:o,barMaxWidth:Math.max(10,t)}}function v(){const e=u*2+w()+i.length*(d+m);k.style.maxHeight=`${ht(r,A,Y(),e)}px`;const n=k.clientWidth,o=pt(s,r,n,e);if(!o)return;const{ctx:t}=o;t.font=`${ot(-1)}px system-ui, sans-serif`,t.textBaseline="middle";const c=xt(r);i.forEach((a,p)=>{const{y:h,barX:B,barMaxWidth:S}=F(p);p===x&&(t.fillStyle=c.bgHover,t.fillRect(0,h-m/2,n,d+m)),t.fillStyle=c.text,t.textAlign="right";const R=a.label.length>16?`${a.label.slice(0,15)}\u2026`:a.label;t.fillText(R,u+z-8,h+d/2),t.fillStyle=c.track,t.fillRect(B,h,S,d);const L=Math.max(1,a.value/M*S);t.fillStyle=G?G(a,p):c.text,t.fillRect(B,h,L,d),t.fillStyle=c.text,t.textAlign="right",t.fillText(N(a),B+S+j,h+d/2)});const b=l.reference?.(i);if(b&&i.length){const{barX:a,barMaxWidth:p}=F(0),h=Math.round(a+Math.max(0,Math.min(1,b.value/M))*p)+.5,B=u+w()-3,S=u+w()+i.length*(d+m)-m,R=(Z,tt,et)=>{t.save(),t.strokeStyle=Z,t.lineWidth=tt,t.setLineDash(et),t.beginPath(),t.moveTo(h,B),t.lineTo(h,S),t.stroke(),t.restore()};R(c.bg,3.5,[]),R(c.text,1.5,[4,3]),t.save(),t.font="600 10px system-ui, sans-serif",t.textBaseline="middle",t.textAlign="left";const L=4,_=t.measureText(b.label).width+L*2,X=13,V=u+w()-X-1;let T=h-_/2;T=Math.max(a,Math.min(T,a+p-_)),t.fillStyle=c.bg,t.strokeStyle=c.border,t.lineWidth=1,t.beginPath(),t.rect(T,V,_,X),t.fill(),t.stroke(),t.fillStyle=c.text,t.fillText(b.label,T+L,V+X/2+.5),t.restore()}}function q(e){if(!l.selfControl)return;l.selfControl.current=e;const n=l.selfControl.onChange(e);i=n.data,M=Math.max(1,...i.map(o=>o.value)),x=-1,n.title&&(y=n.title,E.textContent=y),$(),v()}function J(e){if(!f)return;const n=f.onOpenGroup(e);i=n.data,y=n.title,E.textContent=y,M=Math.max(1,...i.map(o=>o.value)),x=-1,g||(g=!0,W=ut(()=>K(),r.ownerDocument),P.appendChild(W)),$(),v()}function K(){if(!f)return;const e=f.onBack();i=e.data,y=e.title,E.textContent=y,M=Math.max(1,...i.map(n=>n.value)),x=-1,g=!1,W?.remove(),W=null,$(),v()}function O(e){const n=Math.floor((e-u+m/2)/(d+m));return n>=0&&n<i.length?n:-1}s.addEventListener("mousemove",e=>{const n=s.getBoundingClientRect(),o=O(e.clientY-n.top),t=o>=0&&f&&!g&&i[o].itemCount>1,c=o>=0&&(t||(!!l.onOpen||!!l.onSelectBar)&&!t);if(o!==x&&(x=o,s.style.cursor=c?"pointer":"default",v()),o>=0){const b=i[o],a=t?`<br><em>click to see this ${D(f.groupLabelText)} by wafer</em>`:l.onOpen?"<br><em>click to open this wafer</em>":l.onSelectBar?`<br><em>click to ${D(l.selectLabel??"see what it counts")}</em>`:"",p=!t&&l.onWaferContextMenu&&typeof b.key=="number"?bt:"";C.innerHTML=`<strong>${D(b.label)}</strong><br>${D(N(b))}${a}${p}`,C.style.display="block",ft(C,r,e.clientX,e.clientY)}else C.style.display="none"}),s.addEventListener("mouseleave",()=>{x!==-1&&(x=-1,v()),C.style.display="none"}),s.addEventListener("click",e=>{const n=s.getBoundingClientRect(),o=O(e.clientY-n.top);if(o===-1)return;const t=i[o];if(f&&!g&&t.itemCount>1){J(t);return}if(l.onSelectBar){C.style.display="none",l.onSelectBar(t,e);return}l.onOpen?.(t)}),s.addEventListener("contextmenu",e=>{const n=s.getBoundingClientRect(),o=O(e.clientY-n.top);if(o===-1||!l.onWaferContextMenu)return;const t=i[o];f&&!g&&t.itemCount>1||typeof t.key!="number"||(C.style.display="none",l.onWaferContextMenu(t.key,void 0,e))});const Q=ct(r,()=>v());return v(),{card:r,destroy:()=>Q.disconnect()}}
@@ -1,4 +1,4 @@
1
- import { type BinItem } from '../../stats/binPareto.js';
1
+ import { type BinItem, type BinType } from '../../stats/binPareto.js';
2
2
  import { type SaveImageHandler } from './chartShell.js';
3
3
  export interface BinClusterPanelOptions {
4
4
  title?: string;
@@ -7,6 +7,12 @@ export interface BinClusterPanelOptions {
7
7
  items: BinItem[];
8
8
  }[];
9
9
  onSaveImage?: SaveImageHandler;
10
+ /** Click a sub-bar: a bin in one group. The host picks out those dies and opens a menu at the click. */
11
+ onSelectBin?: (sel: {
12
+ bin: number;
13
+ binType: BinType;
14
+ groupKey: string;
15
+ }, e: MouseEvent) => void;
10
16
  /** Document to build this panel's DOM into. Default `document` — pass the
11
17
  * host's own `ownerDocument` when the container might live in a
12
18
  * different document (e.g. a gallery card detached into its own popup
@@ -1 +1 @@
1
- import{buildBinClusterData as H}from"../../stats/binPareto.js";import{SPACE as T,FONT as w,CLR as E}from"../toolbar.js";import{cardShell as L,makeTooltip as $,makeSegmented as D,renderEmptyState as I}from"./chartShell.js";import{renderGroupedBarPlot as M}from"./groupedBarPlot.js";import{escHtml as g}from"../../core/utils.js";const R=90,_=8;export function renderBinClusterPanel(s){const{groups:f,onSaveImage:h}=s;let n="hbin",c=s.title??"Hard bin pareto";const{card:o,heading:S,body:r,controlsRow:y}=L(c,h,s.ownerDocument);y.appendChild(D([["hbin","Hard bins"],["sbin","Soft bins"]],n,e=>{n=e,c=`${n==="hbin"?"Hard":"Soft"} bin pareto`,S.textContent=c,m()},o.ownerDocument));const u=o.ownerDocument.createElement("div");u.textContent="One cluster per bin \xB7 a sub-bar per group",Object.assign(u.style,{color:E.label,fontSize:w.body,marginBottom:T.sm}),o.insertBefore(u,r);const B=$(o);let l=null;function m(){l?.destroy(),l=null,r.innerHTML="";const e=H(f,n);if(e.bins.length===0||e.groups.length===0){I(r,"No bin data available for the current grouping.");return}const p=e.groups,b=e.bins,C=Math.max(1,...b.flatMap(t=>t.counts));l=M(o,r,B,{rows:b.map(t=>({label:t.label,bars:t.counts.map(a=>({fraction:a/C})),trailing:`${t.total}`})),groups:p,labelWidth:R,maxVisibleRows:_,tooltipHtml:(t,a)=>{const i=b[t],d=i.counts[a],x=i.total>0?d/i.total*100:0;return`<strong>${g(i.label)}</strong> \xB7 ${g(String(p[a]))}<br>${d} dies (${x.toFixed(1)}% of bin)`}})}return m(),{card:o,destroy:()=>l?.destroy()}}
1
+ import{buildBinClusterData as x}from"../../stats/binPareto.js";import{SPACE as H,FONT as T,CLR as E}from"../toolbar.js";import{cardShell as L,makeTooltip as $,makeSegmented as D,renderEmptyState as I}from"./chartShell.js";import{renderGroupedBarPlot as R}from"./groupedBarPlot.js";import{escHtml as g}from"../../core/utils.js";const k=90,M=8;export function renderBinClusterPanel(e){const{groups:f,onSaveImage:S}=e;let i="hbin",u=e.title??"Hard bin pareto";const{card:n,heading:h,body:l,controlsRow:B}=L(u,S,e.ownerDocument);B.appendChild(D([["hbin","Hard bins"],["sbin","Soft bins"]],i,o=>{i=o,u=`${i==="hbin"?"Hard":"Soft"} bin pareto`,h.textContent=u,m()},n.ownerDocument));const s=n.ownerDocument.createElement("div");s.dataset.wmapCaption="1",s.textContent="One cluster per bin \xB7 a sub-bar per group"+(e.onSelectBin?" \xB7 click a sub-bar to chart or tabulate its dies":""),Object.assign(s.style,{color:E.label,fontSize:T.body,marginBottom:H.sm}),n.insertBefore(s,l);const C=$(n);let c=null;function m(){c?.destroy(),c=null,l.innerHTML="";const o=x(f,i);if(o.bins.length===0||o.groups.length===0){I(l,"No bin data available for the current grouping.");return}const d=o.groups,b=o.bins,y=Math.max(1,...b.flatMap(t=>t.counts));c=R(n,l,C,{rows:b.map(t=>({label:t.label,bars:t.counts.map(r=>({fraction:r/y})),trailing:`${t.total}`})),groups:d,labelWidth:k,maxVisibleRows:M,onRowClick:e.onSelectBin?(t,r,a)=>e.onSelectBin({bin:b[t].binCode,binType:i,groupKey:d[r]},a):void 0,tooltipHtml:(t,r)=>{const a=b[t],p=a.counts[r],w=a.total>0?p/a.total*100:0;return`<strong>${g(a.label)}</strong> \xB7 ${g(String(d[r]))}<br>${p} dies (${w.toFixed(1)}% of bin)`}})}return m(),{card:n,destroy:()=>c?.destroy()}}
@@ -1 +1 @@
1
- import{buildTestBoxplotData as rt}from"../../stats/boxplot.js";import{SPACE as He,fontPx as _,FONT as ct,CLR as ft}from"../toolbar.js";import{fmt as ge}from"../../renderer/fmt.js";import{fitTicks as ut}from"../../renderer/axisTicks.js";import{QUANTITY as De}from"./palette.js";import{cardShell as mt,observeResize as dt,makeTooltip as xt,positionChartTooltip as pt,makeBackButton as ht,makeLinkedTestSelect as gt,makeToggle as bt,makeLinkedAxisPrefs as yt,renderEmptyState as Ee,fitRowsHeight as Tt,setChartGrow as wt,resolveChartCanvasColors as vt,makeAxisFormat as Oe,horizontalTickSpacing as kt,resolveAxisRange as Mt,shouldIncludeLimitsByDefault as Lt,drawOffAxisLimits as Ct,limitLabelSide as At,limitLines as We,limitExtent as $t,hasBothLimitKinds as St,stackLabelRows as It,strokeLimitLine as Bt,PADDING as M,VALUE_WIDTH as Rt,WAFER_MENU_HINT as Ht,prepareCanvas as Dt}from"./chartShell.js";import{escHtml as L,maxOf as Et,minOf as Ot}from"../../core/utils.js";const C=24,A=5,Pe=110,Wt=12,_e=20;export function renderBoxplotPanel(u){const{title:te="Test value distribution",items:Ge,testDefs:be,onSaveImage:Ne,groups:g,groupLabelText:ne="group",onOpen:Q}=u,Xe=u.openActionLabel??"open that wafer",qe=u.openActionLabel??"open this wafer",{card:a,heading:Ye,body:x,controlsRow:U}=mt(te,Ne,u.ownerDocument);wt(a,"rows"),a.style.flex="0 0 auto",x.style.flex="0 0 auto",a.style.alignSelf="start";const oe=be.filter(i=>i.testNumber!==void 0);let T=u.selectedTestNumber??oe[0]?.testNumber??null,ie=!1,F=0,b=null,D=null,le=null,ye=null;function ze(){const i=!g||g.length===0?"":b??"\0overview";if(le&&ye===i)return le;const p=!g||g.length===0?Ge:b!==null?g.find(v=>v.key===b)?.items??[]:g.map(v=>({label:v.key,dies:v.items.flatMap(S=>S.dies??[])}));return le=p,ye=i,p}function Te(){b!==null&&!D?(D=ht(()=>{we(null),u.onGroupChange?.(null)},a.ownerDocument),U.appendChild(D)):b===null&&D&&(D.remove(),D=null),Ye.textContent=b!==null?`${te} \u2014 ${ne}: ${b}`:te}function we(i){return b===i||i!==null&&!(g??[]).some(p=>p.key===i)?!1:(b=i,Te(),V(),$(),!0)}const ve=gt(oe,T,i=>{T=i,$(),u.onTestChange?.(i)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Qe=ve.el;U.appendChild(Qe),U.appendChild(bt("Log scale",ie,i=>{ie=i,$()},a.ownerDocument));const se=a.ownerDocument.createElement("span");Object.assign(se.style,{display:"inline-flex",gap:He.lg,alignItems:"center"}),U.appendChild(se);const ae=yt(se,u.axisPrefs,i=>{u.onAxisPrefsChange?.(i),$()},a.ownerDocument),re=a.ownerDocument.createElement("div");Object.assign(re.style,{color:ft.label,fontSize:ct.body,marginBottom:He.sm}),a.insertBefore(re,x);function V(){const i=!!g&&g.length>0&&b===null,p=[];i?p.push(`click a ${ne}'s box to see it by wafer`):Q&&p.push(`click a box to ${Xe}`);const v=p.length?`${p[0][0].toUpperCase()}${p[0].slice(1)} \xB7 `:"";re.textContent=`${v}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(F?` \xB7 axis clipped, ${F} value${F===1?"":"s"} outside`:"")}V(),Te();const E=xt(a);let ce=null;function $(){const{includeLimits:i,clipOutliers:p,limits:v}=ae.get();if(V(),x.innerHTML="",oe.length===0||T===null){Ee(x,"No parametric test data available for box plots.");return}const S=ze(),h=rt(S,T),j=be.find(t=>t.testNumber===T),O=j?.unit,fe=We(j,v),{lo:Ve,hi:je}=$t(fe);if(h.every(t=>t.count===0)){Ee(x,"No parametric test data available for box plots.");return}const m=a.ownerDocument.createElement("canvas");m.style.display="block",m.style.cursor="default",x.appendChild(m);const ke=M*2+Math.min(h.length,Wt)*(C+A)+_e;x.style.maxHeight=`${ke}px`,x.style.overflowY="auto",x.style.scrollbarGutter="stable";let G=-1;const ue=h.filter(t=>t.count>0),Me=Ot(ue.map(t=>t.min)),Le=Et(ue.map(t=>t.max)),Ce=i??Lt(Me,Le,Ve,je);ae.sync(Ce,We(j).length>0,St(j));const N=Mt({dataMin:Me,dataMax:Le,limits:fe,includeLimits:Ce,clipOutliers:p,values:ue.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});F=N.clippedCount,V();const W=N.lo,K=N.hi,Ke=K-W||1,J=ie&&W>0,me=J?Math.log10(W):0,Ae=(J?Math.log10(K):0)-me||1,de=Math.max(Math.abs(W),Math.abs(K));function Je(){const t=M+Pe,s=m.clientWidth-t-Rt-M;return{plotX:t,plotMaxWidth:Math.max(10,s)}}function I(t,s,n){return J?s+(Math.log10(t)-me)/Ae*n:s+(t-W)/Ke*n}function Ze(t,s){if(J){const d=[];for(let e=0;e<=4;e++)d.push(Math.pow(10,me+Ae*e/4));return{ticks:d,axis:Oe(de,O)}}const n=ut(W,K,s,kt(t,de,O));return{ticks:n.ticks,axis:Oe(de,O,n.step||void 0)}}function Z(){const t=vt(a),s=M*2+h.length*(C+A)+_e;x.style.maxHeight=`${Tt(a,x,ke,s)}px`;const n=x.clientWidth,d=Dt(m,a,n,s);if(!d)return;const{ctx:e}=d;e.font=`${_(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:c}=Je();h.forEach((o,f)=>{const y=M+f*(C+A),r=y+C/2;f===G&&(e.fillStyle=t.bgHover,e.fillRect(0,y-A/2,n,C+A)),e.fillStyle=t.text,e.textAlign="right";const ee=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(ee,M+Pe-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const it=l+c,q=at=>Math.min(it,Math.max(l,at)),Ie=I(o.min,l,c),Be=I(o.max,l,c),B=q(Ie),Y=q(I(o.q1,l,c)),Re=q(I(o.median,l,c)),he=q(I(o.q3,l,c)),R=q(Be),lt=Ie<B,st=Be>R,H=y+3,z=y+C-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(B,r),e.lineTo(Y,r),e.moveTo(he,r),e.lineTo(R,r);const k=3;lt?(e.moveTo(B+k,r-k),e.lineTo(B,r),e.lineTo(B+k,r+k)):(e.moveTo(B,H),e.lineTo(B,z)),st?(e.moveTo(R-k,r-k),e.lineTo(R,r),e.lineTo(R-k,r+k)):(e.moveTo(R,H),e.lineTo(R,z)),e.stroke(),e.fillStyle=De,e.globalAlpha=.35,e.fillRect(Y,H,Math.max(1,he-Y),z-H),e.globalAlpha=1,e.strokeStyle=De,e.strokeRect(Y,H,Math.max(1,he-Y),z-H),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Re,H),e.lineTo(Re,z),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(ge(o.median,O,"engineering"),l+c+10,r)});const w=M+h.length*(C+A);e.font=`${_(-1)}px system-ui, sans-serif`,Ct(e,N.offAxis,{left:l,right:l+c,top:0,bottom:w},"horizontal",t.limitLine,o=>ge(o,O,"engineering"));const et=new Set(N.offAxis.map(o=>o.value)),Se=fe.filter(o=>!et.has(o.value)).map(o=>{const f=I(o.value,l,c),y=e.measureText(o.label).width,r=At(f,y,l,l+c,o.end==="lo"),ee=r<0?f-3-y:f+3;return{l:o,x:f,dir:r,start:ee,end:ee+y}}),tt=It(Se),nt=_(-1)+2;Se.forEach(({l:o,x:f,dir:y},r)=>{Bt(e,o,t.limitLine,f,0,f,w),e.fillStyle=t.limitLine,e.textAlign=y<0?"right":"left",e.textBaseline="bottom",e.fillText(o.label,f+y*3,w-1-tt[r]*nt)}),e.font=`${_(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,w),e.lineTo(l+c,w),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:ot,axis:pe}=Ze(e,c);for(const o of ot){const f=I(o,l,c);e.beginPath(),e.moveTo(f,w),e.lineTo(f,w+4),e.stroke(),e.fillText(pe.tick(o),f,w+6)}pe.unitLabel&&(e.textAlign="left",e.font=`${_(-1)}px system-ui, sans-serif`,e.fillText(`(${pe.unitLabel})`,l+c+22,w+6),e.font=`${_(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function xe(t){const s=Math.floor((t-M+A/2)/(C+A));return s>=0&&s<h.length?s:-1}function X(t){return ge(t,O,"engineering")}const P=!!g&&g.length>0&&b===null,$e=t=>!P&&!!Q&&S[t]?.waferIndex!==void 0;m.addEventListener("mousemove",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top),d=n>=0&&h[n].count>0&&(P||$e(n));if(n!==G&&(G=n,m.style.cursor=d?"pointer":"default",Z()),n>=0&&h[n].count>0){const e=h[n],l=P?`<br><em>click to see this ${L(ne)} by wafer</em>`:$e(n)?`<br><em>click to ${L(qe)}</em>`:"",c=!P&&u.onWaferContextMenu&&S[n]?.waferIndex!==void 0?Ht:"";E.innerHTML=`<strong>${L(e.label)}</strong> (${e.count} dies)<br>max ${L(X(e.max))}<br>q3 ${L(X(e.q3))}<br>median ${L(X(e.median))}<br>q1 ${L(X(e.q1))}<br>min ${L(X(e.min))}${l}${c}`,E.style.display="block",pt(E,a,t.clientX,t.clientY)}else E.style.display="none"}),m.addEventListener("mouseleave",()=>{G!==-1&&(G=-1,Z()),E.style.display="none"}),m.addEventListener("click",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top);if(n===-1||h[n].count===0)return;if(P){const e=h[n].label;we(e),u.onGroupChange?.(e);return}const d=S[n]?.waferIndex;Q&&d!==void 0&&T!==null&&Q(d,T)}),m.addEventListener("contextmenu",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top),d=n===-1?void 0:S[n]?.waferIndex;P||d===void 0||!u.onWaferContextMenu||(E.style.display="none",u.onWaferContextMenu(d,T??void 0,t))}),ce?.disconnect(),ce=dt(a,()=>Z()),Z()}$();function Ue(i){ve.set(i)&&(T=i,$())}function Fe(i){ae.set(i)&&$()}return{card:a,setTest:Ue,setAxisPrefs:Fe,destroy:()=>ce?.disconnect()}}
1
+ import{buildTestBoxplotData as rt}from"../../stats/boxplot.js";import{SPACE as He,fontPx as _,FONT as ct,CLR as ft}from"../toolbar.js";import{fmt as ge}from"../../renderer/fmt.js";import{fitTicks as ut}from"../../renderer/axisTicks.js";import{QUANTITY as De}from"./palette.js";import{cardShell as mt,observeResize as dt,makeTooltip as xt,positionChartTooltip as pt,makeBackButton as ht,makeLinkedTestSelect as gt,makeToggle as bt,makeLinkedAxisPrefs as yt,renderEmptyState as Ee,fitRowsHeight as Tt,setChartGrow as wt,resolveChartCanvasColors as vt,makeAxisFormat as Oe,horizontalTickSpacing as kt,resolveAxisRange as Mt,shouldIncludeLimitsByDefault as Lt,drawOffAxisLimits as Ct,limitLabelSide as At,limitLines as We,limitExtent as $t,hasBothLimitKinds as St,stackLabelRows as It,strokeLimitLine as Bt,PADDING as M,VALUE_WIDTH as Rt,WAFER_MENU_HINT as Ht,prepareCanvas as Dt}from"./chartShell.js";import{escHtml as L,maxOf as Et,minOf as Ot}from"../../core/utils.js";const C=24,A=5,Pe=110,Wt=12,_e=20;export function renderBoxplotPanel(u){const{title:ne="Test value distribution",items:Ge,testDefs:be,onSaveImage:Ne,groups:g,groupLabelText:oe="group",onOpen:Q}=u,Xe=u.openActionLabel??"open that wafer",qe=u.openActionLabel??"open this wafer",{card:a,heading:Ye,body:x,controlsRow:U}=mt(ne,Ne,u.ownerDocument);wt(a,"rows"),a.style.flex="0 0 auto",x.style.flex="0 0 auto",a.style.alignSelf="start";const ie=be.filter(i=>i.testNumber!==void 0);let T=u.selectedTestNumber??ie[0]?.testNumber??null,le=!1,F=0,b=null,D=null,se=null,ye=null;function ze(){const i=!g||g.length===0?"":b??"\0overview";if(se&&ye===i)return se;const p=!g||g.length===0?Ge:b!==null?g.find(v=>v.key===b)?.items??[]:g.map(v=>({label:v.key,dies:v.items.flatMap(S=>S.dies??[])}));return se=p,ye=i,p}function Te(){b!==null&&!D?(D=ht(()=>{we(null),u.onGroupChange?.(null)},a.ownerDocument),U.appendChild(D)):b===null&&D&&(D.remove(),D=null),Ye.textContent=b!==null?`${ne} \u2014 ${oe}: ${b}`:ne}function we(i){return b===i||i!==null&&!(g??[]).some(p=>p.key===i)?!1:(b=i,Te(),j(),$(),!0)}const ve=gt(ie,T,i=>{T=i,$(),u.onTestChange?.(i)},{maxWidth:"240px",emptyText:"No parametric tests",ownerDocument:a.ownerDocument}),Qe=ve.el;U.appendChild(Qe),U.appendChild(bt("Log scale",le,i=>{le=i,$()},a.ownerDocument));const ae=a.ownerDocument.createElement("span");Object.assign(ae.style,{display:"inline-flex",gap:He.lg,alignItems:"center"}),U.appendChild(ae);const re=yt(ae,u.axisPrefs,i=>{u.onAxisPrefsChange?.(i),$()},a.ownerDocument),V=a.ownerDocument.createElement("div");V.dataset.wmapCaption="1",Object.assign(V.style,{color:ft.label,fontSize:ct.body,marginBottom:He.sm}),a.insertBefore(V,x);function j(){const i=!!g&&g.length>0&&b===null,p=[];i?p.push(`click a ${oe}'s box to see it by wafer`):Q&&p.push(`click a box to ${Xe}`);const v=p.length?`${p[0][0].toUpperCase()}${p[0].slice(1)} \xB7 `:"";V.textContent=`${v}box = Q1\u2013Q3, line = median, whiskers = min/max \xB7 value shown is the median`+(F?` \xB7 axis clipped, ${F} value${F===1?"":"s"} outside`:"")}j(),Te();const E=xt(a);let ce=null;function $(){const{includeLimits:i,clipOutliers:p,limits:v}=re.get();if(j(),x.innerHTML="",ie.length===0||T===null){Ee(x,"No parametric test data available for box plots.");return}const S=ze(),h=rt(S,T),K=be.find(t=>t.testNumber===T),O=K?.unit,fe=We(K,v),{lo:Ve,hi:je}=$t(fe);if(h.every(t=>t.count===0)){Ee(x,"No parametric test data available for box plots.");return}const m=a.ownerDocument.createElement("canvas");m.style.display="block",m.style.cursor="default",x.appendChild(m);const ke=M*2+Math.min(h.length,Wt)*(C+A)+_e;x.style.maxHeight=`${ke}px`,x.style.overflowY="auto",x.style.scrollbarGutter="stable";let G=-1;const ue=h.filter(t=>t.count>0),Me=Ot(ue.map(t=>t.min)),Le=Et(ue.map(t=>t.max)),Ce=i??Lt(Me,Le,Ve,je);re.sync(Ce,We(K).length>0,St(K));const N=Mt({dataMin:Me,dataMax:Le,limits:fe,includeLimits:Ce,clipOutliers:p,values:ue.flatMap(t=>[t.min,t.q1,t.median,t.q3,t.max])});F=N.clippedCount,j();const W=N.lo,J=N.hi,Ke=J-W||1,Z=le&&W>0,me=Z?Math.log10(W):0,Ae=(Z?Math.log10(J):0)-me||1,de=Math.max(Math.abs(W),Math.abs(J));function Je(){const t=M+Pe,s=m.clientWidth-t-Rt-M;return{plotX:t,plotMaxWidth:Math.max(10,s)}}function I(t,s,n){return Z?s+(Math.log10(t)-me)/Ae*n:s+(t-W)/Ke*n}function Ze(t,s){if(Z){const d=[];for(let e=0;e<=4;e++)d.push(Math.pow(10,me+Ae*e/4));return{ticks:d,axis:Oe(de,O)}}const n=ut(W,J,s,kt(t,de,O));return{ticks:n.ticks,axis:Oe(de,O,n.step||void 0)}}function ee(){const t=vt(a),s=M*2+h.length*(C+A)+_e;x.style.maxHeight=`${Tt(a,x,ke,s)}px`;const n=x.clientWidth,d=Dt(m,a,n,s);if(!d)return;const{ctx:e}=d;e.font=`${_(-1)}px system-ui, sans-serif`,e.textBaseline="middle";const{plotX:l,plotMaxWidth:c}=Je();h.forEach((o,f)=>{const y=M+f*(C+A),r=y+C/2;f===G&&(e.fillStyle=t.bgHover,e.fillRect(0,y-A/2,n,C+A)),e.fillStyle=t.text,e.textAlign="right";const te=o.label.length>16?`${o.label.slice(0,15)}\u2026`:o.label;if(e.fillText(te,M+Pe-8,r),o.count===0){e.fillStyle=t.textMuted,e.textAlign="left",e.fillText("no data",l,r);return}const it=l+c,q=at=>Math.min(it,Math.max(l,at)),Ie=I(o.min,l,c),Be=I(o.max,l,c),B=q(Ie),Y=q(I(o.q1,l,c)),Re=q(I(o.median,l,c)),he=q(I(o.q3,l,c)),R=q(Be),lt=Ie<B,st=Be>R,H=y+3,z=y+C-3;e.strokeStyle=t.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(B,r),e.lineTo(Y,r),e.moveTo(he,r),e.lineTo(R,r);const k=3;lt?(e.moveTo(B+k,r-k),e.lineTo(B,r),e.lineTo(B+k,r+k)):(e.moveTo(B,H),e.lineTo(B,z)),st?(e.moveTo(R-k,r-k),e.lineTo(R,r),e.lineTo(R-k,r+k)):(e.moveTo(R,H),e.lineTo(R,z)),e.stroke(),e.fillStyle=De,e.globalAlpha=.35,e.fillRect(Y,H,Math.max(1,he-Y),z-H),e.globalAlpha=1,e.strokeStyle=De,e.strokeRect(Y,H,Math.max(1,he-Y),z-H),e.strokeStyle=t.text,e.lineWidth=2,e.beginPath(),e.moveTo(Re,H),e.lineTo(Re,z),e.stroke(),e.lineWidth=1,e.fillStyle=t.textMuted,e.textAlign="left",e.fillText(ge(o.median,O,"engineering"),l+c+10,r)});const w=M+h.length*(C+A);e.font=`${_(-1)}px system-ui, sans-serif`,Ct(e,N.offAxis,{left:l,right:l+c,top:0,bottom:w},"horizontal",t.limitLine,o=>ge(o,O,"engineering"));const et=new Set(N.offAxis.map(o=>o.value)),Se=fe.filter(o=>!et.has(o.value)).map(o=>{const f=I(o.value,l,c),y=e.measureText(o.label).width,r=At(f,y,l,l+c,o.end==="lo"),te=r<0?f-3-y:f+3;return{l:o,x:f,dir:r,start:te,end:te+y}}),tt=It(Se),nt=_(-1)+2;Se.forEach(({l:o,x:f,dir:y},r)=>{Bt(e,o,t.limitLine,f,0,f,w),e.fillStyle=t.limitLine,e.textAlign=y<0?"right":"left",e.textBaseline="bottom",e.fillText(o.label,f+y*3,w-1-tt[r]*nt)}),e.font=`${_(-1)}px system-ui, sans-serif`,e.strokeStyle=t.border,e.lineWidth=1,e.beginPath(),e.moveTo(l,w),e.lineTo(l+c,w),e.stroke(),e.fillStyle=t.textMuted,e.textAlign="center",e.textBaseline="top";const{ticks:ot,axis:pe}=Ze(e,c);for(const o of ot){const f=I(o,l,c);e.beginPath(),e.moveTo(f,w),e.lineTo(f,w+4),e.stroke(),e.fillText(pe.tick(o),f,w+6)}pe.unitLabel&&(e.textAlign="left",e.font=`${_(-1)}px system-ui, sans-serif`,e.fillText(`(${pe.unitLabel})`,l+c+22,w+6),e.font=`${_(-1)}px system-ui, sans-serif`),e.textBaseline="middle"}function xe(t){const s=Math.floor((t-M+A/2)/(C+A));return s>=0&&s<h.length?s:-1}function X(t){return ge(t,O,"engineering")}const P=!!g&&g.length>0&&b===null,$e=t=>!P&&!!Q&&S[t]?.waferIndex!==void 0;m.addEventListener("mousemove",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top),d=n>=0&&h[n].count>0&&(P||$e(n));if(n!==G&&(G=n,m.style.cursor=d?"pointer":"default",ee()),n>=0&&h[n].count>0){const e=h[n],l=P?`<br><em>click to see this ${L(oe)} by wafer</em>`:$e(n)?`<br><em>click to ${L(qe)}</em>`:"",c=!P&&u.onWaferContextMenu&&S[n]?.waferIndex!==void 0?Ht:"";E.innerHTML=`<strong>${L(e.label)}</strong> (${e.count} dies)<br>max ${L(X(e.max))}<br>q3 ${L(X(e.q3))}<br>median ${L(X(e.median))}<br>q1 ${L(X(e.q1))}<br>min ${L(X(e.min))}${l}${c}`,E.style.display="block",pt(E,a,t.clientX,t.clientY)}else E.style.display="none"}),m.addEventListener("mouseleave",()=>{G!==-1&&(G=-1,ee()),E.style.display="none"}),m.addEventListener("click",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top);if(n===-1||h[n].count===0)return;if(P){const e=h[n].label;we(e),u.onGroupChange?.(e);return}const d=S[n]?.waferIndex;Q&&d!==void 0&&T!==null&&Q(d,T)}),m.addEventListener("contextmenu",t=>{const s=m.getBoundingClientRect(),n=xe(t.clientY-s.top),d=n===-1?void 0:S[n]?.waferIndex;P||d===void 0||!u.onWaferContextMenu||(E.style.display="none",u.onWaferContextMenu(d,T??void 0,t))}),ce?.disconnect(),ce=dt(a,()=>ee()),ee()}$();function Ue(i){ve.set(i)&&(T=i,$())}function Fe(i){re.set(i)&&$()}return{card:a,setTest:Ue,setAxisPrefs:Fe,destroy:()=>ce?.disconnect()}}
@@ -1 +1 @@
1
- import{buildCapabilityData as ve}from"../../stats/capability.js";import{capabilityColor as U}from"./palette.js";import{LEADING as Se,SPACE as T,fontPx as J,FONT as Z,CLR as _}from"../toolbar.js";import{cardShell as Me,isExpandedCard as Te,chartFillHeight as $e,applyCanvasFlow as Ce,observeResize as Le,makeTooltip as ke,positionChartTooltip as Pe,renderEmptyState as Ee,resolveChartCanvasColors as De,chartSwatchCss as Ae,prepareCanvas as We,chartDpr as we}from"./chartShell.js";import{fmt as Ne}from"../../renderer/fmt.js";import{escHtml as V}from"../../core/utils.js";import{DERIVED_MARK as ee,DERIVED_KEY as le}from"../../renderer/testLabel.js";function N(v){return v==="spec"?{lo:"LSL",hi:"USL",noun:"spec limits"}:{lo:"Lo limit",hi:"Hi limit",noun:"test limits"}}function ae(v){let D;for(const z of v){if(!z.hasSpec)continue;const A=z.limitBasis??"test";if(D===void 0)D=A;else if(D!==A)return"mixed"}return D}const Ie=30,re=160,ce=90,me=28,C=42;function j(v){return v===null?"\u2014":v.toFixed(2)}export function renderCapabilityPanel(v){const{title:D="Process capability",items:z,testDefs:A,onSaveImage:de,onSelectTest:X}=v;let q=v.selectedTestNumber??null;const{card:m,body:L}=Me(D,de,v.ownerDocument),pe=A.some(t=>t.testNumber!==void 0);L.style.overflowX="auto";function he(){return z}const W=m.ownerDocument.createElement("div");Object.assign(W.style,{display:"flex",flexDirection:"column",gap:T.xs,marginBottom:T.sm}),m.insertBefore(W,L);const I=ke(m);let F=()=>{};function fe(t,l,S,f,u){W.innerHTML="";const $=m.ownerDocument.createElement("span");Object.assign($.style,{display:"inline-flex",alignItems:"center",gap:T.sm,color:_.value,fontSize:Z.body,fontWeight:"500"});const d=S-t,y=l>0?` \xB7 ${l} without both limits`:"",M=m.ownerDocument.createElement("span");M.textContent=d>0?`${t} of ${S} tests shown${y} \xB7 ${d} excluded (no recorded values)`:`${t} test${t!==1?"s":""} shown${y}`,$.appendChild(M),W.appendChild($);const k=l>=t,b=m.ownerDocument.createElement("div");b.textContent=k?"No test has both limits, so each is normalised to its own observed range (min = 0, max = 1) and sorted most-variable first. Ppk needs limits, so none is shown.":(u==="mixed"?"Normalised to each test's spec limits (LSL = 0, USL = 1), or its test limits (Lo limit = 0, Hi limit = 1) where it has no spec limits, worst Ppk first. ":`Normalised to ${N(u).noun} (${N(u).lo} = 0, ${N(u).hi} = 1), worst Ppk first. `)+"Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(b.style,{color:_.label,fontSize:Z.body,lineHeight:Se.base,marginTop:T.xs,maxWidth:"78ch"}),W.appendChild(b),W.appendChild(ue(m.ownerDocument,l>0,k,f>0))}function ue(t,l,S=!1,f=!1){const u=t.createElement("div");Object.assign(u.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${T.xs} ${T.xl}`,marginTop:T.xl,marginBottom:T.xs,fontSize:Z.body,color:_.label});const $=S?[]:[{color:U(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:U(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:U(.5),label:"Poor \xB7 Ppk < 1.0"}];l&&$.push({color:_.label,label:"No limits \xB7 no Ppk",dashed:!0});for(const d of $){const y=t.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:T.sm});const M=t.createElement("span");M.style.cssText=Ae(d.color,d.dashed?"outline":"solid");const k=t.createElement("span");k.textContent=d.label,y.append(M,k),u.appendChild(y)}if(f){const d=t.createElement("span");Object.assign(d.style,{display:"inline-flex",alignItems:"center",gap:T.sm});const y=t.createElement("span");y.textContent=ee,Object.assign(y.style,{color:_.value,fontWeight:"600"});const M=t.createElement("span");M.textContent=le,d.append(y,M),u.appendChild(d)}return u}function be(t){if(L.innerHTML="",t.length===0)return Ee(L,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const l=m.ownerDocument.createElement("canvas");l.style.display="block",l.style.cursor="default",L.appendChild(l);const S=t.length,f=Math.min(0,...t.map(n=>n.min)),u=Math.max(1,...t.map(n=>n.max)),$=(u-f)*.05||.1,d=f-$,M=u+$-d||1;function k(n){const o=Math.floor(n/S);return Math.max(Ie,Te(m)?o:Math.min(re,o))}function b(n,o,i){return o+(1-(n-d)/M)*i}function ge(n,o,i,s,P){const x=t.filter(g=>g.hasSpec).length,w=x===t.length,p=x===0,E=ae(t),e=E==="mixed"?{lo:"Lo",hi:"Hi",noun:"limits"}:N(E),h=p?"max":E==="spec"?e.hi:"Hi",a=p?"min":E==="spec"?e.lo:"Lo",H=w?`normalised to ${e.noun}`:p?"normalised to range":"normalised (per test)";n.font=`${J(-1)}px system-ui, sans-serif`,n.textAlign="right",n.textBaseline="middle";for(const[g,r]of[[1,h],[0,a]]){const c=b(g,i,s);c<i-1||c>i+s+1||(n.strokeStyle=o.limitLine,n.lineWidth=1,n.beginPath(),n.moveTo(C-5,c),n.lineTo(C,c),n.stroke(),n.fillStyle=o.textMuted,n.fillText(r,C-8,c))}n.save(),n.translate(9,i+s/2),n.rotate(-Math.PI/2),n.textAlign="center",n.fillStyle=o.textMuted,n.fillText(H,0,0),n.restore()}let B=-1;function G(){Ce(l);const n=L.clientWidth,o=k(n-C),i=o*S,s=me,P=C+i,x=$e(m,L,l,me+200+ce),w=Math.max(s+60,x-ce),p=w-s,E=We(l,m,P,x);if(!E)return;const{ctx:e}=E,h=De(m);t.forEach((a,H)=>{const g=C+H*o,r=g+o/2,c=Math.max(4,Math.min(o,re)*.55);if(H===B&&(e.fillStyle=h.bgHover,e.fillRect(g,s,o,w-s)),t[H].testNumber===q&&(e.fillStyle=h.accent,e.fillRect(g,w-2,o,2)),a.hasSpec){const oe=b(0,s,p),se=b(1,s,p);e.strokeStyle=h.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(g,oe),e.lineTo(g+o,oe),e.moveTo(g,se),e.lineTo(g+o,se),e.stroke(),e.setLineDash([])}const K=a.hasSpec?U(a.ppk):h.textMuted,Q=b(a.min,s,p),R=b(a.q1,s,p),ne=b(a.median,s,p),O=b(a.q3,s,p),Y=b(a.max,s,p);e.strokeStyle=h.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(r,Q),e.lineTo(r,R),e.moveTo(r,O),e.lineTo(r,Y),e.moveTo(r-c/4,Q),e.lineTo(r+c/4,Q),e.moveTo(r-c/4,Y),e.lineTo(r+c/4,Y),e.stroke(),e.fillStyle=K,e.globalAlpha=.5,e.fillRect(r-c/2,Math.min(R,O),c,Math.max(1,Math.abs(O-R))),e.globalAlpha=1,e.strokeStyle=K,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(r-c/2,Math.min(R,O),c,Math.max(1,Math.abs(O-R))),e.setLineDash([]),e.strokeStyle=h.text,e.lineWidth=2,e.beginPath(),e.moveTo(r-c/2,ne),e.lineTo(r+c/2,ne),e.stroke(),e.lineWidth=1,e.font=`${J(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?K:h.textMuted,e.fillText(a.hasSpec?j(a.ppk):"\u2014",r,s-8);const ie=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(r,w+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${J(-1)}px system-ui, sans-serif`,e.fillStyle=h.textMuted,e.fillText(ie,0,0),a.derived&&(e.fillStyle=h.text,e.fillText(`${ee} `,-e.measureText(ie).width,0)),e.restore()}),e.strokeStyle=h.border,e.lineWidth=1,e.strokeRect(C,s,i,p),ge(e,h,s,p,i)}function te(n){const o=l.getBoundingClientRect(),i=L.clientWidth,s=k(i-C),P=(n.clientX-o.left)*(l.width/we(l)/o.width)-C,x=Math.floor(P/s);return x>=0&&x<S?x:-1}return l.addEventListener("mousemove",n=>{const o=te(n);if(o!==B&&(B=o,G()),o===-1){I.style.display="none";return}const i=t[o],s=x=>V(Ne(x,i.unit)),P=i.derived?`<em>${ee} ${V(le)}</em>`+(i.expression?`<br><code>${V(i.expression)}</code>`:"")+"<br>":"";I.innerHTML=`<strong>${V(i.label)}</strong> (n=${i.n})<br>${P}`+(i.hasSpec?`${N(i.limitBasis).lo} ${s(i.lsl)} \xB7 ${N(i.limitBasis).hi} ${s(i.usl)}<br>mean ${s(i.mean)}<br>Cp ${j(i.cp)} \xB7 Cpk ${j(i.cpk)}<br>Pp ${j(i.pp)} \xB7 Ppk ${j(i.ppk)}`:`<em>No limits \u2014 sorted by variability</em><br>mean ${s(i.mean)} \xB7 stddev ${s(i.stdOverall)}`)+(X?"<br><em>click to view in boxplot</em>":""),I.style.display="block",Pe(I,m,n.clientX,n.clientY),l.style.cursor=X?"pointer":"default"}),l.addEventListener("mouseleave",()=>{B!==-1&&(B=-1,G()),I.style.display="none"}),l.addEventListener("click",n=>{if(!X)return;const o=te(n);o!==-1&&X(t[o].testNumber)}),G}function ye(){const t=ve(he(),A),l=A.filter(f=>f.testNumber!==void 0).length,S=t.filter(f=>!f.hasSpec).length;fe(t.length,S,l,t.filter(f=>f.derived).length,ae(t)),F=be(t),F()}const xe=Le(m,()=>F());return ye(),{card:m,hasData:pe,setTest:t=>{q!==t&&(q=t,F())},destroy:()=>xe.disconnect()}}
1
+ import{buildCapabilityData as ve}from"../../stats/capability.js";import{capabilityColor as U}from"./palette.js";import{LEADING as Se,SPACE as T,fontPx as J,FONT as Z,CLR as _}from"../toolbar.js";import{cardShell as Me,isExpandedCard as Te,chartFillHeight as $e,applyCanvasFlow as Ce,observeResize as Le,makeTooltip as ke,positionChartTooltip as Pe,renderEmptyState as Ee,resolveChartCanvasColors as we,chartSwatchCss as De,prepareCanvas as Ae,chartDpr as We}from"./chartShell.js";import{fmt as Ne}from"../../renderer/fmt.js";import{escHtml as V}from"../../core/utils.js";import{DERIVED_MARK as ee,DERIVED_KEY as le}from"../../renderer/testLabel.js";function N(S){return S==="spec"?{lo:"LSL",hi:"USL",noun:"spec limits"}:{lo:"Lo limit",hi:"Hi limit",noun:"test limits"}}function ae(S){let w;for(const z of S){if(!z.hasSpec)continue;const D=z.limitBasis??"test";if(w===void 0)w=D;else if(w!==D)return"mixed"}return w}const Ie=30,re=160,ce=90,me=28,C=42;function j(S){return S===null?"\u2014":S.toFixed(2)}export function renderCapabilityPanel(S){const{title:w="Process capability",items:z,testDefs:D,onSaveImage:de,onSelectTest:X}=S;let q=S.selectedTestNumber??null;const{card:m,body:L}=Me(w,de,S.ownerDocument),pe=D.some(t=>t.testNumber!==void 0);L.style.overflowX="auto";function he(){return z}const A=m.ownerDocument.createElement("div");Object.assign(A.style,{display:"flex",flexDirection:"column",gap:T.xs,marginBottom:T.sm}),m.insertBefore(A,L);const I=ke(m);let F=()=>{};function fe(t,l,M,u,b){A.innerHTML="";const $=m.ownerDocument.createElement("span");Object.assign($.style,{display:"inline-flex",alignItems:"center",gap:T.sm,color:_.value,fontSize:Z.body,fontWeight:"500"});const d=M-t,y=l>0?` \xB7 ${l} without both limits`:"",x=m.ownerDocument.createElement("span");x.dataset.wmapCaption="1",x.textContent=d>0?`${t} of ${M} tests shown${y} \xB7 ${d} excluded (no recorded values)`:`${t} test${t!==1?"s":""} shown${y}`,$.appendChild(x),A.appendChild($);const k=l>=t,p=m.ownerDocument.createElement("div");p.dataset.wmapCaption="1",p.textContent=k?"No test has both limits, so each is normalised to its own observed range (min = 0, max = 1) and sorted most-variable first. Ppk needs limits, so none is shown.":(b==="mixed"?"Normalised to each test's spec limits (LSL = 0, USL = 1), or its test limits (Lo limit = 0, Hi limit = 1) where it has no spec limits, worst Ppk first. ":`Normalised to ${N(b).noun} (${N(b).lo} = 0, ${N(b).hi} = 1), worst Ppk first. `)+"Tests without both limits are normalised to their own observed range and drawn muted/dashed.",Object.assign(p.style,{color:_.label,fontSize:Z.body,lineHeight:Se.base,marginTop:T.xs,maxWidth:"78ch"}),A.appendChild(p),A.appendChild(ue(m.ownerDocument,l>0,k,u>0))}function ue(t,l,M=!1,u=!1){const b=t.createElement("div");Object.assign(b.style,{display:"flex",flexWrap:"wrap",alignItems:"center",gap:`${T.xs} ${T.xl}`,marginTop:T.xl,marginBottom:T.xs,fontSize:Z.body,color:_.label});const $=M?[]:[{color:U(2),label:"Capable \xB7 Ppk \u2265 1.33"},{color:U(1.1),label:"Marginal \xB7 Ppk \u2265 1.0"},{color:U(.5),label:"Poor \xB7 Ppk < 1.0"}];l&&$.push({color:_.label,label:"No limits \xB7 no Ppk",dashed:!0});for(const d of $){const y=t.createElement("span");Object.assign(y.style,{display:"inline-flex",alignItems:"center",gap:T.sm});const x=t.createElement("span");x.style.cssText=De(d.color,d.dashed?"outline":"solid");const k=t.createElement("span");k.textContent=d.label,y.append(x,k),b.appendChild(y)}if(u){const d=t.createElement("span");Object.assign(d.style,{display:"inline-flex",alignItems:"center",gap:T.sm});const y=t.createElement("span");y.textContent=ee,Object.assign(y.style,{color:_.value,fontWeight:"600"});const x=t.createElement("span");x.textContent=le,d.append(y,x),b.appendChild(d)}return b}function be(t){if(L.innerHTML="",t.length===0)return Ee(L,"No parametric tests have any recorded values.",{maxWidth:"480px"}),()=>{};const l=m.ownerDocument.createElement("canvas");l.style.display="block",l.style.cursor="default",L.appendChild(l);const M=t.length,u=Math.min(0,...t.map(n=>n.min)),b=Math.max(1,...t.map(n=>n.max)),$=(b-u)*.05||.1,d=u-$,x=b+$-d||1;function k(n){const o=Math.floor(n/M);return Math.max(Ie,Te(m)?o:Math.min(re,o))}function p(n,o,i){return o+(1-(n-d)/x)*i}function ge(n,o,i,s,P){const g=t.filter(v=>v.hasSpec).length,W=g===t.length,h=g===0,E=ae(t),e=E==="mixed"?{lo:"Lo",hi:"Hi",noun:"limits"}:N(E),f=h?"max":E==="spec"?e.hi:"Hi",a=h?"min":E==="spec"?e.lo:"Lo",H=W?`normalised to ${e.noun}`:h?"normalised to range":"normalised (per test)";n.font=`${J(-1)}px system-ui, sans-serif`,n.textAlign="right",n.textBaseline="middle";for(const[v,r]of[[1,f],[0,a]]){const c=p(v,i,s);c<i-1||c>i+s+1||(n.strokeStyle=o.limitLine,n.lineWidth=1,n.beginPath(),n.moveTo(C-5,c),n.lineTo(C,c),n.stroke(),n.fillStyle=o.textMuted,n.fillText(r,C-8,c))}n.save(),n.translate(9,i+s/2),n.rotate(-Math.PI/2),n.textAlign="center",n.fillStyle=o.textMuted,n.fillText(H,0,0),n.restore()}let B=-1;function G(){Ce(l);const n=L.clientWidth,o=k(n-C),i=o*M,s=me,P=C+i,g=$e(m,L,l,me+200+ce),W=Math.max(s+60,g-ce),h=W-s,E=Ae(l,m,P,g);if(!E)return;const{ctx:e}=E,f=we(m);t.forEach((a,H)=>{const v=C+H*o,r=v+o/2,c=Math.max(4,Math.min(o,re)*.55);if(H===B&&(e.fillStyle=f.bgHover,e.fillRect(v,s,o,W-s)),t[H].testNumber===q&&(e.fillStyle=f.accent,e.fillRect(v,W-2,o,2)),a.hasSpec){const oe=p(0,s,h),se=p(1,s,h);e.strokeStyle=f.limitLine,e.setLineDash([3,3]),e.lineWidth=1,e.beginPath(),e.moveTo(v,oe),e.lineTo(v+o,oe),e.moveTo(v,se),e.lineTo(v+o,se),e.stroke(),e.setLineDash([])}const K=a.hasSpec?U(a.ppk):f.textMuted,Q=p(a.min,s,h),R=p(a.q1,s,h),ne=p(a.median,s,h),O=p(a.q3,s,h),Y=p(a.max,s,h);e.strokeStyle=f.textMuted,e.lineWidth=1,e.beginPath(),e.moveTo(r,Q),e.lineTo(r,R),e.moveTo(r,O),e.lineTo(r,Y),e.moveTo(r-c/4,Q),e.lineTo(r+c/4,Q),e.moveTo(r-c/4,Y),e.lineTo(r+c/4,Y),e.stroke(),e.fillStyle=K,e.globalAlpha=.5,e.fillRect(r-c/2,Math.min(R,O),c,Math.max(1,Math.abs(O-R))),e.globalAlpha=1,e.strokeStyle=K,a.hasSpec||e.setLineDash([4,2]),e.strokeRect(r-c/2,Math.min(R,O),c,Math.max(1,Math.abs(O-R))),e.setLineDash([]),e.strokeStyle=f.text,e.lineWidth=2,e.beginPath(),e.moveTo(r-c/2,ne),e.lineTo(r+c/2,ne),e.stroke(),e.lineWidth=1,e.font=`${J(-1)}px system-ui, sans-serif`,e.textAlign="center",e.textBaseline="alphabetic",e.fillStyle=a.hasSpec?K:f.textMuted,e.fillText(a.hasSpec?j(a.ppk):"\u2014",r,s-8);const ie=a.label.length>12?`${a.label.slice(0,11)}\u2026`:a.label;e.save(),e.translate(r,W+6),e.rotate(-Math.PI/4),e.textAlign="right",e.textBaseline="middle",e.font=`${J(-1)}px system-ui, sans-serif`,e.fillStyle=f.textMuted,e.fillText(ie,0,0),a.derived&&(e.fillStyle=f.text,e.fillText(`${ee} `,-e.measureText(ie).width,0)),e.restore()}),e.strokeStyle=f.border,e.lineWidth=1,e.strokeRect(C,s,i,h),ge(e,f,s,h,i)}function te(n){const o=l.getBoundingClientRect(),i=L.clientWidth,s=k(i-C),P=(n.clientX-o.left)*(l.width/We(l)/o.width)-C,g=Math.floor(P/s);return g>=0&&g<M?g:-1}return l.addEventListener("mousemove",n=>{const o=te(n);if(o!==B&&(B=o,G()),o===-1){I.style.display="none";return}const i=t[o],s=g=>V(Ne(g,i.unit)),P=i.derived?`<em>${ee} ${V(le)}</em>`+(i.expression?`<br><code>${V(i.expression)}</code>`:"")+"<br>":"";I.innerHTML=`<strong>${V(i.label)}</strong> (n=${i.n})<br>${P}`+(i.hasSpec?`${N(i.limitBasis).lo} ${s(i.lsl)} \xB7 ${N(i.limitBasis).hi} ${s(i.usl)}<br>mean ${s(i.mean)}<br>Cp ${j(i.cp)} \xB7 Cpk ${j(i.cpk)}<br>Pp ${j(i.pp)} \xB7 Ppk ${j(i.ppk)}`:`<em>No limits \u2014 sorted by variability</em><br>mean ${s(i.mean)} \xB7 stddev ${s(i.stdOverall)}`)+(X?"<br><em>click to view in boxplot</em>":""),I.style.display="block",Pe(I,m,n.clientX,n.clientY),l.style.cursor=X?"pointer":"default"}),l.addEventListener("mouseleave",()=>{B!==-1&&(B=-1,G()),I.style.display="none"}),l.addEventListener("click",n=>{if(!X)return;const o=te(n);o!==-1&&X(t[o].testNumber)}),G}function ye(){const t=ve(he(),D),l=D.filter(u=>u.testNumber!==void 0).length,M=t.filter(u=>!u.hasSpec).length;fe(t.length,M,l,t.filter(u=>u.derived).length,ae(t)),F=be(t),F()}const xe=Le(m,()=>F());return ye(),{card:m,hasData:pe,setTest:t=>{q!==t&&(q=t,F())},destroy:()=>xe.disconnect()}}