react-cheminfo 0.70.0 → 0.71.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 (213) hide show
  1. package/README.md +57 -2
  2. package/lib/about/core/providers.d.ts +6 -0
  3. package/lib/about/core/providers.d.ts.map +1 -1
  4. package/lib/about/core/providers.js +7 -0
  5. package/lib/about/core/providers.js.map +1 -1
  6. package/lib/about/ui/AboutProvidedBy.js +34 -0
  7. package/lib/about/ui/AboutProvidedBy.js.map +1 -1
  8. package/lib/download/core/figurePng.d.ts +14 -0
  9. package/lib/download/core/figurePng.d.ts.map +1 -1
  10. package/lib/download/core/figurePng.js +21 -6
  11. package/lib/download/core/figurePng.js.map +1 -1
  12. package/lib/download/core/svgToPng.d.ts.map +1 -1
  13. package/lib/download/core/svgToPng.js +4 -2
  14. package/lib/download/core/svgToPng.js.map +1 -1
  15. package/lib/format/core/relativeTime.d.ts +8 -3
  16. package/lib/format/core/relativeTime.d.ts.map +1 -1
  17. package/lib/format/core/relativeTime.js +19 -7
  18. package/lib/format/core/relativeTime.js.map +1 -1
  19. package/lib/host/core/connectHost.d.ts +25 -34
  20. package/lib/host/core/connectHost.d.ts.map +1 -1
  21. package/lib/host/core/connectHost.js +43 -89
  22. package/lib/host/core/connectHost.js.map +1 -1
  23. package/lib/host/core/hostOutputs.d.ts +58 -0
  24. package/lib/host/core/hostOutputs.d.ts.map +1 -0
  25. package/lib/host/core/hostOutputs.js +151 -0
  26. package/lib/host/core/hostOutputs.js.map +1 -0
  27. package/lib/host/core/index.d.ts +9 -3
  28. package/lib/host/core/index.d.ts.map +1 -1
  29. package/lib/host/core/index.js +4 -1
  30. package/lib/host/core/index.js.map +1 -1
  31. package/lib/host/core/irKinds.d.ts +124 -0
  32. package/lib/host/core/irKinds.d.ts.map +1 -0
  33. package/lib/host/core/irKinds.js +46 -0
  34. package/lib/host/core/irKinds.js.map +1 -0
  35. package/lib/host/core/kinds.d.ts +13 -5
  36. package/lib/host/core/kinds.d.ts.map +1 -1
  37. package/lib/host/core/kinds.js +11 -5
  38. package/lib/host/core/kinds.js.map +1 -1
  39. package/lib/host/core/protocol.d.ts +50 -7
  40. package/lib/host/core/protocol.d.ts.map +1 -1
  41. package/lib/host/core/protocol.js +10 -6
  42. package/lib/host/core/protocol.js.map +1 -1
  43. package/lib/host/core/selectInputs.d.ts +37 -0
  44. package/lib/host/core/selectInputs.d.ts.map +1 -0
  45. package/lib/host/core/selectInputs.js +110 -0
  46. package/lib/host/core/selectInputs.js.map +1 -0
  47. package/lib/host/testing/sciPeaks.d.ts +31 -0
  48. package/lib/host/testing/sciPeaks.d.ts.map +1 -1
  49. package/lib/host/testing/sciPeaks.js +73 -0
  50. package/lib/host/testing/sciPeaks.js.map +1 -1
  51. package/lib/host/testing/standInHost.d.ts +9 -1
  52. package/lib/host/testing/standInHost.d.ts.map +1 -1
  53. package/lib/host/testing/standInHost.js +19 -1
  54. package/lib/host/testing/standInHost.js.map +1 -1
  55. package/lib/host/ui/useHostConnection.d.ts +13 -5
  56. package/lib/host/ui/useHostConnection.d.ts.map +1 -1
  57. package/lib/host/ui/useHostConnection.js +4 -3
  58. package/lib/host/ui/useHostConnection.js.map +1 -1
  59. package/lib/host-testing.d.ts +2 -2
  60. package/lib/host-testing.d.ts.map +1 -1
  61. package/lib/host-testing.js +2 -2
  62. package/lib/host-testing.js.map +1 -1
  63. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  64. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  65. package/lib/ir/core/handPicking.d.ts +58 -0
  66. package/lib/ir/core/handPicking.d.ts.map +1 -0
  67. package/lib/ir/core/handPicking.js +130 -0
  68. package/lib/ir/core/handPicking.js.map +1 -0
  69. package/lib/ir/core/index.d.ts +3 -0
  70. package/lib/ir/core/index.d.ts.map +1 -1
  71. package/lib/ir/core/index.js +3 -0
  72. package/lib/ir/core/index.js.map +1 -1
  73. package/lib/ir/core/irBandState.d.ts +57 -0
  74. package/lib/ir/core/irBandState.d.ts.map +1 -0
  75. package/lib/ir/core/irBandState.js +119 -0
  76. package/lib/ir/core/irBandState.js.map +1 -0
  77. package/lib/ir/core/irCommands.d.ts +11 -0
  78. package/lib/ir/core/irCommands.d.ts.map +1 -1
  79. package/lib/ir/core/irCommands.js +6 -0
  80. package/lib/ir/core/irCommands.js.map +1 -1
  81. package/lib/ir/core/irPeaks.d.ts +28 -0
  82. package/lib/ir/core/irPeaks.d.ts.map +1 -0
  83. package/lib/ir/core/irPeaks.js +66 -0
  84. package/lib/ir/core/irPeaks.js.map +1 -0
  85. package/lib/ir/core/irReducer.d.ts +8 -0
  86. package/lib/ir/core/irReducer.d.ts.map +1 -1
  87. package/lib/ir/core/irReducer.js +15 -32
  88. package/lib/ir/core/irReducer.js.map +1 -1
  89. package/lib/ir/core/irSpectrum.d.ts +15 -0
  90. package/lib/ir/core/irSpectrum.d.ts.map +1 -1
  91. package/lib/ir/core/irSpectrum.js.map +1 -1
  92. package/lib/ir/core/irState.d.ts +9 -6
  93. package/lib/ir/core/irState.d.ts.map +1 -1
  94. package/lib/ir/core/irState.js +10 -6
  95. package/lib/ir/core/irState.js.map +1 -1
  96. package/lib/ir/core/pickBands.d.ts +14 -0
  97. package/lib/ir/core/pickBands.d.ts.map +1 -1
  98. package/lib/ir/core/pickBands.js +39 -33
  99. package/lib/ir/core/pickBands.js.map +1 -1
  100. package/lib/ir/core/ramanBands.d.ts +9 -1
  101. package/lib/ir/core/ramanBands.d.ts.map +1 -1
  102. package/lib/ir/core/ramanBands.js +2 -2
  103. package/lib/ir/core/ramanBands.js.map +1 -1
  104. package/lib/ir/core/spectrumBands.d.ts +46 -0
  105. package/lib/ir/core/spectrumBands.d.ts.map +1 -0
  106. package/lib/ir/core/spectrumBands.js +98 -0
  107. package/lib/ir/core/spectrumBands.js.map +1 -0
  108. package/lib/ir/ui/BandRow.d.ts +5 -0
  109. package/lib/ir/ui/BandRow.d.ts.map +1 -1
  110. package/lib/ir/ui/BandRow.js +4 -3
  111. package/lib/ir/ui/BandRow.js.map +1 -1
  112. package/lib/ir/ui/BandsPanel.d.ts +5 -0
  113. package/lib/ir/ui/BandsPanel.d.ts.map +1 -1
  114. package/lib/ir/ui/BandsPanel.js +36 -16
  115. package/lib/ir/ui/BandsPanel.js.map +1 -1
  116. package/lib/ir/ui/BandsToolbar.d.ts +9 -0
  117. package/lib/ir/ui/BandsToolbar.d.ts.map +1 -0
  118. package/lib/ir/ui/BandsToolbar.js +61 -0
  119. package/lib/ir/ui/BandsToolbar.js.map +1 -0
  120. package/lib/ir/ui/IrCanvas.d.ts +2 -1
  121. package/lib/ir/ui/IrCanvas.d.ts.map +1 -1
  122. package/lib/ir/ui/IrCanvas.js +19 -5
  123. package/lib/ir/ui/IrCanvas.js.map +1 -1
  124. package/lib/ir/ui/IrChart.d.ts.map +1 -1
  125. package/lib/ir/ui/IrChart.js +8 -1
  126. package/lib/ir/ui/IrChart.js.map +1 -1
  127. package/lib/ir/ui/IrDocumentationDialog.js +1 -1
  128. package/lib/ir/ui/IrDocumentationDialog.js.map +1 -1
  129. package/lib/ir/ui/IrEditor.d.ts +4 -2
  130. package/lib/ir/ui/IrEditor.d.ts.map +1 -1
  131. package/lib/ir/ui/IrEditor.js.map +1 -1
  132. package/lib/ir/ui/IrShell.d.ts.map +1 -1
  133. package/lib/ir/ui/IrShell.js +6 -0
  134. package/lib/ir/ui/IrShell.js.map +1 -1
  135. package/lib/ir/ui/IrStatusBar.d.ts.map +1 -1
  136. package/lib/ir/ui/IrStatusBar.js +1 -1
  137. package/lib/ir/ui/IrStatusBar.js.map +1 -1
  138. package/lib/ir/ui/IrToolbar.d.ts +4 -3
  139. package/lib/ir/ui/IrToolbar.d.ts.map +1 -1
  140. package/lib/ir/ui/IrToolbar.js +5 -4
  141. package/lib/ir/ui/IrToolbar.js.map +1 -1
  142. package/lib/ir/ui/VibrationalEditor.d.ts +2 -1
  143. package/lib/ir/ui/VibrationalEditor.d.ts.map +1 -1
  144. package/lib/ir/ui/VibrationalEditor.js.map +1 -1
  145. package/lib/ir/ui/irActions.d.ts +5 -0
  146. package/lib/ir/ui/irActions.d.ts.map +1 -1
  147. package/lib/ir/ui/irActions.js +6 -0
  148. package/lib/ir/ui/irActions.js.map +1 -1
  149. package/lib/ir/ui/irChartProps.d.ts +4 -1
  150. package/lib/ir/ui/irChartProps.d.ts.map +1 -1
  151. package/lib/ir/ui/useIrEditor.d.ts +5 -11
  152. package/lib/ir/ui/useIrEditor.d.ts.map +1 -1
  153. package/lib/ir/ui/useIrEditor.js +17 -18
  154. package/lib/ir/ui/useIrEditor.js.map +1 -1
  155. package/lib/locales/de.json +63 -0
  156. package/lib/locales/en.json +63 -0
  157. package/lib/locales/es.json +63 -0
  158. package/lib/locales/fr.json +63 -0
  159. package/lib/locales/it.json +63 -0
  160. package/lib/slides/ui/slideSanitizeSchema.d.ts +3 -2
  161. package/lib/slides/ui/slideSanitizeSchema.d.ts.map +1 -1
  162. package/lib/slides/ui/slideSanitizeSchema.js +82 -2
  163. package/lib/slides/ui/slideSanitizeSchema.js.map +1 -1
  164. package/package.json +1 -1
  165. package/src/about/core/providers.ts +7 -0
  166. package/src/about/ui/AboutProvidedBy.tsx +49 -0
  167. package/src/download/core/figurePng.ts +21 -8
  168. package/src/download/core/svgToPng.ts +3 -4
  169. package/src/format/core/relativeTime.ts +29 -7
  170. package/src/host/core/connectHost.ts +77 -120
  171. package/src/host/core/hostOutputs.ts +197 -0
  172. package/src/host/core/index.ts +13 -7
  173. package/src/host/core/irKinds.ts +92 -0
  174. package/src/host/core/kinds.ts +16 -6
  175. package/src/host/core/protocol.ts +50 -8
  176. package/src/host/core/selectInputs.ts +145 -0
  177. package/src/host/testing/sciPeaks.ts +140 -0
  178. package/src/host/testing/standInHost.ts +30 -5
  179. package/src/host/ui/useHostConnection.ts +17 -8
  180. package/src/host-testing.ts +4 -0
  181. package/src/ir/core/handPicking.ts +154 -0
  182. package/src/ir/core/index.ts +14 -0
  183. package/src/ir/core/irBandState.ts +155 -0
  184. package/src/ir/core/irCommands.ts +7 -0
  185. package/src/ir/core/irPeaks.ts +79 -0
  186. package/src/ir/core/irReducer.ts +27 -34
  187. package/src/ir/core/irSpectrum.ts +16 -0
  188. package/src/ir/core/irState.ts +15 -8
  189. package/src/ir/core/pickBands.ts +46 -39
  190. package/src/ir/core/ramanBands.ts +2 -2
  191. package/src/ir/core/spectrumBands.ts +113 -0
  192. package/src/ir/ui/BandRow.tsx +20 -2
  193. package/src/ir/ui/BandsPanel.tsx +46 -88
  194. package/src/ir/ui/BandsToolbar.tsx +152 -0
  195. package/src/ir/ui/IrCanvas.tsx +22 -11
  196. package/src/ir/ui/IrChart.tsx +10 -1
  197. package/src/ir/ui/IrDocumentationDialog.tsx +7 -0
  198. package/src/ir/ui/IrEditor.tsx +4 -2
  199. package/src/ir/ui/IrShell.tsx +5 -0
  200. package/src/ir/ui/IrStatusBar.tsx +4 -1
  201. package/src/ir/ui/IrToolbar.tsx +14 -5
  202. package/src/ir/ui/VibrationalEditor.tsx +2 -1
  203. package/src/ir/ui/irActions.ts +11 -0
  204. package/src/ir/ui/irChartProps.ts +4 -1
  205. package/src/ir/ui/useIrEditor.ts +21 -21
  206. package/src/locales/de.json +63 -0
  207. package/src/locales/en.json +63 -0
  208. package/src/locales/es.json +63 -0
  209. package/src/locales/fr.json +63 -0
  210. package/src/locales/it.json +63 -0
  211. package/src/slides/ui/slideSanitizeSchema.ts +86 -2
  212. package/styles/chrome.css +7 -0
  213. package/styles/slides.css +47 -0
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Picking bands by hand.
3
+ *
4
+ * Where a click lands is not where the band is: it is taken to the top of the
5
+ * band it was aimed at, and the band is read off the spectrum there exactly as
6
+ * automatic picking reads one — the same values, and the same strength letter.
7
+ */
8
+
9
+ import { peakPicking } from 'ir-spectrum';
10
+ import {
11
+ xMinMaxValues,
12
+ xyMaxClosestYPoint,
13
+ xyMaxYPoint,
14
+ xyMinYPoint,
15
+ } from 'ml-spectra-processing';
16
+
17
+ import type { IrBand, IrBandStrength } from './irBand.ts';
18
+ import type { IrSpectrum } from './irSpectrum.ts';
19
+ import { infraredMeasurement } from './pickBands.ts';
20
+ import { intensityStrength } from './ramanBands.ts';
21
+
22
+ /**
23
+ * What a click while picking by hand makes of a spectrum's bands: the band it
24
+ * landed on taken away, or else the band under it added.
25
+ * @param spectrum - The spectrum picked on.
26
+ * @param wavenumber - Where the click landed, in cm⁻¹.
27
+ * @param reach - How close to a band counts as on it, and how far either side
28
+ * to look for one, in cm⁻¹: a few pixels' worth at the zoom of the chart.
29
+ * @returns The new bands, or the spectrum's own list when nothing changed.
30
+ */
31
+ export function editBandsAt(
32
+ spectrum: IrSpectrum,
33
+ wavenumber: number,
34
+ reach: number,
35
+ ): IrBand[] {
36
+ const bands = spectrum.bands ?? [];
37
+ let hit: IrBand | null = null;
38
+ let nearest = Math.abs(reach);
39
+ for (const band of bands) {
40
+ const distance = Math.abs(band.wavenumber - wavenumber);
41
+ if (distance <= nearest) {
42
+ nearest = distance;
43
+ hit = band;
44
+ }
45
+ }
46
+ if (hit !== null) return removeBand(bands, hit.wavenumber);
47
+ const band = pickBandAt(spectrum, wavenumber, reach);
48
+ return band === null ? bands : addBand(bands, band);
49
+ }
50
+
51
+ /**
52
+ * The band a click asks for: the top of the band under it.
53
+ *
54
+ * The highest point within reach of the click first, then uphill from there to
55
+ * the top: a click lands on a band's flank as often as on its top, and a flank
56
+ * carries bumps of noise that a climb from the click alone would stop at. A
57
+ * band is a maximum of absorbance, or of intensity for a Raman spectrum,
58
+ * whichever way up the chart is drawn.
59
+ * @param spectrum - The spectrum picked on.
60
+ * @param wavenumber - Where the click landed, in cm⁻¹.
61
+ * @param reach - How far either side of it to look, in cm⁻¹.
62
+ * @returns The band, or `null` when the spectrum carries no axis a band is a
63
+ * maximum of, or is flat within reach.
64
+ */
65
+ export function pickBandAt(
66
+ spectrum: IrSpectrum,
67
+ wavenumber: number,
68
+ reach: number,
69
+ ): IrBand | null {
70
+ const { wavenumber: x, intensity, absorbance } = spectrum;
71
+ const y = intensity ?? absorbance;
72
+ if (y === undefined || x.length < 2 || !Number.isFinite(wavenumber)) {
73
+ return null;
74
+ }
75
+ const around = Number.isFinite(reach) ? Math.abs(reach) : 0;
76
+ const within = { from: wavenumber - around, to: wavenumber + around };
77
+ const near = xyMaxYPoint({ x, y }, within);
78
+ // A stretch that does not rise at all has no band to climb to.
79
+ if (around > 0 && near.y <= xyMinYPoint({ x, y }, within).y) return null;
80
+ const top = xyMaxClosestYPoint({ x, y }, { targetIndex: near.index });
81
+ return bandAtIndex(spectrum, top.index);
82
+ }
83
+
84
+ /**
85
+ * The band at one sample of a spectrum, read the way automatic picking reads
86
+ * one.
87
+ * @param spectrum - The spectrum.
88
+ * @param index - The sample.
89
+ * @returns The band, or `null` when there is no such sample or no axis to read
90
+ * it on.
91
+ */
92
+ export function bandAtIndex(
93
+ spectrum: IrSpectrum,
94
+ index: number,
95
+ ): IrBand | null {
96
+ const { id, wavenumber, absorbance, transmittance, intensity } = spectrum;
97
+ const at = wavenumber[index];
98
+ if (at === undefined) return null;
99
+ if (intensity !== undefined) {
100
+ const value = intensity[index] ?? Number.NaN;
101
+ const { min, max } = xMinMaxValues(intensity);
102
+ return {
103
+ spectrumId: id,
104
+ wavenumber: at,
105
+ intensity: value,
106
+ strength: intensityStrength(value, min, max),
107
+ };
108
+ }
109
+ if (absorbance === undefined || transmittance === undefined) return null;
110
+ // `ir-spectrum` reads the sample and works out the letter, as it does for
111
+ // every band it picks itself.
112
+ const peak = peakPicking(
113
+ infraredMeasurement(wavenumber, absorbance, transmittance),
114
+ at,
115
+ { yVariable: 'a' },
116
+ );
117
+ if (peak === undefined) return null;
118
+ return {
119
+ spectrumId: id,
120
+ wavenumber: at,
121
+ absorbance: peak.absorbance,
122
+ transmittance: peak.transmittance * 100,
123
+ strength: peak.kind as IrBandStrength,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * The bands with one more, in its place.
129
+ * @param bands - The bands, ascending.
130
+ * @param band - The band to add.
131
+ * @returns A new list, or the same one when a band already sits there.
132
+ */
133
+ export function addBand(bands: IrBand[], band: IrBand): IrBand[] {
134
+ let index = 0;
135
+ while (
136
+ index < bands.length &&
137
+ (bands[index]?.wavenumber ?? 0) < band.wavenumber
138
+ ) {
139
+ index++;
140
+ }
141
+ if (bands[index]?.wavenumber === band.wavenumber) return bands;
142
+ return bands.toSpliced(index, 0, band);
143
+ }
144
+
145
+ /**
146
+ * The bands without the one at a wavenumber.
147
+ * @param bands - The bands.
148
+ * @param wavenumber - Where the band to take away sits, exactly.
149
+ * @returns A new list, or the same one when no band sits there.
150
+ */
151
+ export function removeBand(bands: IrBand[], wavenumber: number): IrBand[] {
152
+ const index = bands.findIndex((band) => band.wavenumber === wavenumber);
153
+ return index === -1 ? bands : bands.toSpliced(index, 1);
154
+ }
@@ -24,6 +24,14 @@ export type { BandLabelPlan, BandLabelPlanOptions } from './bandLabelPlan.ts';
24
24
  export { planBandLabels } from './bandLabelPlan.ts';
25
25
  export { detectIrFormat } from './detectIrFormat.ts';
26
26
  export { downloadIrSpectra, downloadIrSpectrum } from './downloadSpectra.ts';
27
+ export {
28
+ addBand,
29
+ bandAtIndex,
30
+ editBandsAt,
31
+ pickBandAt,
32
+ removeBand,
33
+ } from './handPicking.ts';
34
+ export { bandsFromPeaks, peaksFromBands } from './irPeaks.ts';
27
35
  export type { IrJcampReadOptions } from './fromJcamp.ts';
28
36
  export type { IrSpcReadOptions } from './fromSpc.ts';
29
37
  export type { IrBand, IrBandStrength } from './irBand.ts';
@@ -124,6 +132,12 @@ export { loadIrSpectra } from './loadIrSpectra.ts';
124
132
  export { BAND_HIT_RADIUS, nearestBandAt } from './nearestBand.ts';
125
133
  export type { PickBandsOptions } from './pickBands.ts';
126
134
  export { pickBands } from './pickBands.ts';
135
+ export {
136
+ orderedBands,
137
+ repickBands,
138
+ samePicking,
139
+ withBands,
140
+ } from './spectrumBands.ts';
127
141
  export {
128
142
  DEFAULT_DATA_TYPE,
129
143
  JCAMP_EXTENSION,
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The changes of the viewer's state that are about bands: spectra arriving
3
+ * with theirs or without, a list picked by hand, a list found automatically
4
+ * again, and new picking settings, which pick again every list that was found
5
+ * automatically.
6
+ *
7
+ * Apart from the reducer, which routes these actions here; like it, each
8
+ * returns the state itself when it has nothing to do.
9
+ */
10
+
11
+ import type { IrBand } from './irBand.ts';
12
+ import type { IrSpectrum } from './irSpectrum.ts';
13
+ import type { IrSettings, IrState, PartialIrState } from './irState.ts';
14
+ import { mergeIrSettings } from './irState.ts';
15
+ import type { PickBandsOptions } from './pickBands.ts';
16
+ import {
17
+ orderedBands,
18
+ repickBands,
19
+ samePicking,
20
+ withBands,
21
+ } from './spectrumBands.ts';
22
+
23
+ /**
24
+ * Spectra arriving, each carrying its bands: its own, or picked now.
25
+ * @param state - The state they arrive in.
26
+ * @param spectra - The spectra.
27
+ * @returns A new list.
28
+ */
29
+ export function arriving(
30
+ state: IrState,
31
+ spectra: readonly IrSpectrum[],
32
+ ): IrSpectrum[] {
33
+ const { picking } = state.settings;
34
+ return spectra.map((spectrum) => withBands(spectrum, picking));
35
+ }
36
+
37
+ /**
38
+ * Give one spectrum bands picked by hand.
39
+ * @param state - The state in force.
40
+ * @param id - Which spectrum.
41
+ * @param bands - Its bands, in any order.
42
+ * @returns The new state, or the old one when they were its own already.
43
+ */
44
+ export function setSpectrumBands(
45
+ state: IrState,
46
+ id: string,
47
+ bands: IrBand[],
48
+ ): IrState {
49
+ return mapSpectrum(state, id, (spectrum) => {
50
+ const ordered = orderedBands(spectrum.id, bands);
51
+ return ordered === spectrum.bands && spectrum.bandsByHand === true
52
+ ? spectrum
53
+ : { ...spectrum, bands: ordered, bandsByHand: true };
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Have one spectrum's bands found automatically again.
59
+ * @param state - The state in force.
60
+ * @param id - Which spectrum.
61
+ * @returns The new state, or the old one when they already were.
62
+ */
63
+ export function resetSpectrumBands(state: IrState, id: string): IrState {
64
+ const { picking } = state.settings;
65
+ return mapSpectrum(state, id, (spectrum) =>
66
+ spectrum.bandsByHand
67
+ ? repickBands({ ...spectrum, bandsByHand: false }, picking)
68
+ : spectrum,
69
+ );
70
+ }
71
+
72
+ /**
73
+ * Fold a change into the workspace, and pick again what it changes.
74
+ * @param state - The state in force.
75
+ * @param change - What to change about the workspace.
76
+ * @returns The new state.
77
+ */
78
+ export function changeSettings(
79
+ state: IrState,
80
+ change: Partial<IrSettings>,
81
+ ): IrState {
82
+ const settings = mergeIrSettings(state.settings, change);
83
+ const { spectra } = state.data;
84
+ const picked = pickedAgain(spectra, state.settings.picking, settings.picking);
85
+ return {
86
+ ...state,
87
+ data: picked === spectra ? state.data : { spectra: picked },
88
+ settings,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Take a whole state, as a host restoring one supplies it.
94
+ * @param state - The state in force.
95
+ * @param restored - The parts the host supplies.
96
+ * @returns The new state.
97
+ */
98
+ export function restoreState(
99
+ state: IrState,
100
+ restored: PartialIrState,
101
+ ): IrState {
102
+ const { data, view, settings } = restored;
103
+ const merged = mergeIrSettings(state.settings, settings);
104
+ const spectra =
105
+ data === undefined
106
+ ? state.data.spectra
107
+ : arriving({ ...state, settings: merged }, data.spectra);
108
+ return {
109
+ data: {
110
+ spectra: pickedAgain(spectra, state.settings.picking, merged.picking),
111
+ },
112
+ view: { ...state.view, ...view },
113
+ settings: merged,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Change one spectrum, leaving the state alone when it did not change.
119
+ * @param state - The state in force.
120
+ * @param id - Which spectrum.
121
+ * @param change - What to make of it; return it unchanged for no change.
122
+ * @returns The new state, or the old one.
123
+ */
124
+ export function mapSpectrum(
125
+ state: IrState,
126
+ id: string,
127
+ change: (spectrum: IrSpectrum) => IrSpectrum,
128
+ ): IrState {
129
+ let changed = false;
130
+ const spectra = state.data.spectra.map((spectrum) => {
131
+ if (spectrum.id !== id) return spectrum;
132
+ const next = change(spectrum);
133
+ if (next !== spectrum) changed = true;
134
+ return next;
135
+ });
136
+ if (!changed) return state;
137
+ return { ...state, data: { spectra }, view: { ...state.view, error: null } };
138
+ }
139
+
140
+ /**
141
+ * The spectra under new picking settings: those found automatically picked
142
+ * again.
143
+ * @param spectra - The spectra.
144
+ * @param before - The settings they were picked with.
145
+ * @param after - The settings now in force.
146
+ * @returns The same list when the settings pick as before.
147
+ */
148
+ function pickedAgain(
149
+ spectra: IrSpectrum[],
150
+ before: PickBandsOptions,
151
+ after: PickBandsOptions,
152
+ ): IrSpectrum[] {
153
+ if (samePicking(before, after)) return spectra;
154
+ return spectra.map((spectrum) => repickBands(spectrum, after));
155
+ }
@@ -86,6 +86,13 @@ export const irCommands = {
86
86
  'A drag zooms to exactly the rectangle it sweeps out, both axes at once — for looking into a baseline or a shoulder rather than across the spectrum. It is the only drag on percent transmittance.',
87
87
  gestures: [{ title: 'Over the chart', shortcuts: ['Drag a rectangle'] }],
88
88
  },
89
+ pickTool: {
90
+ title: 'Pick bands by hand',
91
+ shortcut: { keys: ['k'] },
92
+ description:
93
+ 'A click adds the band under it to the selected spectrum, at its top; a click on a marked band takes it away. A drag still zooms, and bands picked by hand stay put when the threshold moves.',
94
+ gestures: [{ title: 'Over the chart', shortcuts: ['Click'] }],
95
+ },
89
96
  zoomIn: {
90
97
  title: 'Zoom in',
91
98
  shortcut: { keys: ['+', '='] },
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The bands of an infrared spectrum as a notebook writes them, and back: the
3
+ * peaks of the host protocol's `spectra.ir.jdx` and `spectra.ir.peaks`.
4
+ *
5
+ * The two differ in two ways only: a notebook's transmittance is a fraction
6
+ * where the viewer holds a percentage, and it calls a band's strength `kind`.
7
+ */
8
+
9
+ import { xFindClosestIndex } from 'ml-spectra-processing';
10
+
11
+ import type { IrPeakValue } from '../../host/core/irKinds.ts';
12
+
13
+ import { bandAtIndex } from './handPicking.ts';
14
+ import type { IrBand, IrBandStrength } from './irBand.ts';
15
+ import type { IrSpectrum } from './irSpectrum.ts';
16
+ import { orderedBands } from './spectrumBands.ts';
17
+
18
+ /**
19
+ * The bands a notebook handed over with a spectrum.
20
+ *
21
+ * Each stays where the notebook put it, with the values it gave; a value it
22
+ * left out is read off the spectrum at the nearest sample, and a strength
23
+ * letter that is not one of `w`, `m` and `S` is worked out again.
24
+ * @param spectrum - The spectrum they were picked on.
25
+ * @param peaks - The peaks, in any order.
26
+ * @returns The bands, ascending, one per wavenumber.
27
+ */
28
+ export function bandsFromPeaks(
29
+ spectrum: IrSpectrum,
30
+ peaks: readonly IrPeakValue[],
31
+ ): IrBand[] {
32
+ const bands: IrBand[] = [];
33
+ for (const peak of peaks) {
34
+ const { wavenumber, absorbance, transmittance, kind } = peak;
35
+ if (!Number.isFinite(wavenumber)) continue;
36
+ const read = bandAtIndex(
37
+ spectrum,
38
+ xFindClosestIndex(spectrum.wavenumber, wavenumber),
39
+ );
40
+ if (read === null) continue;
41
+ bands.push({
42
+ ...read,
43
+ wavenumber,
44
+ ...(absorbance === undefined ? {} : { absorbance }),
45
+ ...(transmittance === undefined
46
+ ? {}
47
+ : { transmittance: transmittance * 100 }),
48
+ strength: isStrength(kind) ? kind : read.strength,
49
+ });
50
+ }
51
+ return orderedBands(spectrum.id, bands);
52
+ }
53
+
54
+ /**
55
+ * Bands as a notebook keeps them.
56
+ * @param bands - The bands, as the viewer holds them.
57
+ * @returns The peaks, in the same order.
58
+ */
59
+ export function peaksFromBands(bands: readonly IrBand[]): IrPeakValue[] {
60
+ const peaks: IrPeakValue[] = [];
61
+ for (const band of bands) {
62
+ const { wavenumber, absorbance, transmittance, strength } = band;
63
+ peaks.push({
64
+ wavenumber,
65
+ ...(absorbance === undefined ? {} : { absorbance }),
66
+ ...(transmittance === undefined
67
+ ? {}
68
+ : { transmittance: transmittance / 100 }),
69
+ kind: strength,
70
+ });
71
+ }
72
+ return peaks;
73
+ }
74
+
75
+ function isStrength(kind: string | undefined): kind is IrBandStrength {
76
+ return kind !== undefined && STRENGTHS.includes(kind);
77
+ }
78
+
79
+ const STRENGTHS: readonly string[] = ['w', 'm', 'S'] satisfies IrBandStrength[];
@@ -13,10 +13,19 @@
13
13
  * closed.
14
14
  */
15
15
 
16
+ import type { IrBand } from './irBand.ts';
17
+ import {
18
+ arriving,
19
+ changeSettings,
20
+ mapSpectrum,
21
+ resetSpectrumBands,
22
+ restoreState,
23
+ setSpectrumBands,
24
+ } from './irBandState.ts';
16
25
  import type { IrPanelId } from './irSidePanels.ts';
17
26
  import type { IrDomain, IrSpectrum } from './irSpectrum.ts';
18
27
  import type { IrSettings, IrState, IrTool, PartialIrState } from './irState.ts';
19
- import { isIrWindow, mergeIrSettings } from './irState.ts';
28
+ import { isIrWindow } from './irState.ts';
20
29
 
21
30
  /** Everything that can be asked of the viewer. */
22
31
  export type IrAction =
@@ -26,6 +35,8 @@ export type IrAction =
26
35
  | { type: 'renameSpectrum'; id: string; name: string }
27
36
  | { type: 'setSpectrumColor'; id: string; color: string }
28
37
  | { type: 'setSpectrumVisible'; id: string; visible: boolean }
38
+ | { type: 'setBands'; id: string; bands: IrBand[] }
39
+ | { type: 'resetBands'; id: string }
29
40
  | { type: 'selectSpectrum'; id: string | null }
30
41
  | { type: 'setDomain'; domain: IrDomain }
31
42
  | { type: 'resetDomain' }
@@ -46,7 +57,10 @@ export function irReducer(state: IrState, action: IrAction): IrState {
46
57
  switch (action.type) {
47
58
  case 'addSpectra': {
48
59
  if (action.spectra.length === 0) return state;
49
- const spectra = [...state.data.spectra, ...action.spectra];
60
+ const spectra = [
61
+ ...state.data.spectra,
62
+ ...arriving(state, action.spectra),
63
+ ];
50
64
  return {
51
65
  ...state,
52
66
  data: { spectra },
@@ -65,7 +79,7 @@ export function irReducer(state: IrState, action: IrAction): IrState {
65
79
  case 'setSpectra': {
66
80
  return {
67
81
  ...state,
68
- data: { spectra: [...action.spectra] },
82
+ data: { spectra: arriving(state, action.spectra) },
69
83
  view: {
70
84
  ...state.view,
71
85
  selectedId: action.spectra[0]?.id ?? null,
@@ -122,6 +136,14 @@ export function irReducer(state: IrState, action: IrAction): IrState {
122
136
  );
123
137
  }
124
138
 
139
+ case 'setBands': {
140
+ return setSpectrumBands(state, action.id, action.bands);
141
+ }
142
+
143
+ case 'resetBands': {
144
+ return resetSpectrumBands(state, action.id);
145
+ }
146
+
125
147
  case 'selectSpectrum': {
126
148
  if (state.view.selectedId === action.id) return state;
127
149
  return { ...state, view: { ...state.view, selectedId: action.id } };
@@ -162,17 +184,11 @@ export function irReducer(state: IrState, action: IrAction): IrState {
162
184
  }
163
185
 
164
186
  case 'setSettings': {
165
- const settings = mergeIrSettings(state.settings, action.settings);
166
- return { ...state, settings };
187
+ return changeSettings(state, action.settings);
167
188
  }
168
189
 
169
190
  case 'loadState': {
170
- const { data, view, settings } = action.state;
171
- return {
172
- data: data === undefined ? state.data : { spectra: [...data.spectra] },
173
- view: { ...state.view, ...view },
174
- settings: mergeIrSettings(state.settings, settings),
175
- };
191
+ return restoreState(state, action.state);
176
192
  }
177
193
 
178
194
  case 'clear': {
@@ -191,29 +207,6 @@ export function irReducer(state: IrState, action: IrAction): IrState {
191
207
  }
192
208
  }
193
209
 
194
- /**
195
- * Change one spectrum, leaving the state alone when it did not change.
196
- * @param state - The state in force.
197
- * @param id - Which spectrum.
198
- * @param change - What to make of it; return it unchanged for no change.
199
- * @returns The new state, or the old one.
200
- */
201
- function mapSpectrum(
202
- state: IrState,
203
- id: string,
204
- change: (spectrum: IrSpectrum) => IrSpectrum,
205
- ): IrState {
206
- let changed = false;
207
- const spectra = state.data.spectra.map((spectrum) => {
208
- if (spectrum.id !== id) return spectrum;
209
- const next = change(spectrum);
210
- if (next !== spectrum) changed = true;
211
- return next;
212
- });
213
- if (!changed) return state;
214
- return { ...state, data: { spectra }, view: { ...state.view, error: null } };
215
- }
216
-
217
210
  /**
218
211
  * Refuse an action, saying why.
219
212
  * @param state - The state in force.
@@ -25,6 +25,8 @@ import type { DataXY } from 'cheminfo-types';
25
25
 
26
26
  import type { ChartDomain } from '../../chart/core/chartDomain.ts';
27
27
 
28
+ import type { IrBand } from './irBand.ts';
29
+
28
30
  /**
29
31
  * One trace of a measurement: wavenumber against whichever value axis is drawn.
30
32
  *
@@ -150,6 +152,20 @@ export interface IrSpectrum {
150
152
  * @default false
151
153
  */
152
154
  excludeFromDomain?: boolean;
155
+ /**
156
+ * The bands picked on it, ascending by wavenumber. The viewer picks them
157
+ * when a spectrum arrives without any; a host may hand them over with it, as
158
+ * a notebook that keeps them does.
159
+ * @default undefined
160
+ */
161
+ bands?: IrBand[];
162
+ /**
163
+ * Whether `bands` were picked by hand — edited in the viewer, or handed over
164
+ * — rather than found with the picking settings, which re-pick only a list
165
+ * that was not.
166
+ * @default false
167
+ */
168
+ bandsByHand?: boolean;
153
169
  }
154
170
 
155
171
  /**
@@ -7,10 +7,10 @@
7
7
  * `settings` is the workspace, which is how every spectrum is drawn whichever
8
8
  * ones happen to be open.
9
9
  *
10
- * The bands are **not** in here. They are derived from the spectra and the
11
- * picking settings, so holding them would mean holding an answer that has to be
12
- * kept in step with its own inputs — `useIrEditor` recomputes them instead, and
13
- * a stale band is then impossible rather than merely unlikely.
10
+ * The bands are in `data`, each spectrum carrying its own: picked when it
11
+ * arrives, picked again by the reducer when the settings change while they were
12
+ * found automatically, and left alone once picked by hand. That is what lets a
13
+ * band be added or taken away, and a list be handed to a host and back.
14
14
  */
15
15
 
16
16
  import type { IrPanelId, IrSidePanelId } from './irSidePanels.ts';
@@ -18,6 +18,7 @@ import type { IrDomain, IrMode, IrSpectrum } from './irSpectrum.ts';
18
18
  import type { IrTechnique } from './irTechnique.ts';
19
19
  import { defaultIrMode, irModeFor, offersIrMode } from './irTechnique.ts';
20
20
  import type { PickBandsOptions } from './pickBands.ts';
21
+ import { withBands } from './spectrumBands.ts';
21
22
 
22
23
  /** The document: what a file holds. */
23
24
  export interface IrData {
@@ -25,8 +26,11 @@ export interface IrData {
25
26
  spectra: IrSpectrum[];
26
27
  }
27
28
 
28
- /** What a drag across the chart is for while it is chosen. */
29
- export type IrTool = 'read' | 'box';
29
+ /**
30
+ * What a drag across the chart is for while it is chosen, and for `pick`, what
31
+ * a click is: a band added where it lands, or taken away.
32
+ */
33
+ export type IrTool = 'read' | 'box' | 'pick';
30
34
 
31
35
  /** Where you are in the viewer. */
32
36
  export interface IrView {
@@ -165,8 +169,11 @@ export function createIrState(options: CreateIrStateOptions = {}): IrState {
165
169
  technique,
166
170
  mode: defaultIrMode(technique),
167
171
  };
172
+ const merged = mergeIrSettings(opened, settings);
168
173
  return {
169
- data: { spectra: [...spectra] },
174
+ data: {
175
+ spectra: spectra.map((spectrum) => withBands(spectrum, merged.picking)),
176
+ },
170
177
  view: {
171
178
  selectedId: spectra[0]?.id ?? null,
172
179
  domain: null,
@@ -174,7 +181,7 @@ export function createIrState(options: CreateIrStateOptions = {}): IrState {
174
181
  openPanelIds: [...(panelIds ?? DEFAULT_SIDE_PANELS)],
175
182
  error: null,
176
183
  },
177
- settings: mergeIrSettings(opened, settings),
184
+ settings: merged,
178
185
  };
179
186
  }
180
187