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
@@ -21,6 +21,13 @@ import type { IrSpectrum } from './irSpectrum.ts';
21
21
  import type { PeakPickingOptions } from './ramanBands.ts';
22
22
  import { ramanBands } from './ramanBands.ts';
23
23
 
24
+ /**
25
+ * How many points the picker smooths over before it looks for a band. `ml-gsd`
26
+ * refuses a trace shorter than that — a few points typed in by hand — rather
27
+ * than finding no band in it.
28
+ */
29
+ const SMOOTHING_WINDOW = 9;
30
+
24
31
  /** How the bands are picked. */
25
32
  export interface PickBandsOptions {
26
33
  /**
@@ -88,7 +95,7 @@ export function pickBands(
88
95
  minPeakWidth = 4,
89
96
  limit,
90
97
  } = options;
91
- if (spectrum.wavenumber.length < 2) return [];
98
+ if (spectrum.wavenumber.length < SMOOTHING_WINDOW) return [];
92
99
 
93
100
  const picking: PeakPickingOptions = {
94
101
  minMaxRatio: minRelativeHeight,
@@ -109,43 +116,6 @@ export function pickBands(
109
116
  return kept.toSorted(byAscendingWavenumber);
110
117
  }
111
118
 
112
- /**
113
- * The bands of an infrared spectrum, picked on its absorbance.
114
- * @param spectrum - The spectrum to pick from.
115
- * @param picking - Where to pick, and how finely.
116
- * @returns The bands, in the order they were found; none when the spectrum
117
- * carries only one of its two axes, since the strength is read off the other.
118
- */
119
- function infraredBands(
120
- spectrum: IrSpectrum,
121
- picking: PeakPickingOptions,
122
- ): IrBand[] {
123
- const { id, wavenumber, absorbance, transmittance } = spectrum;
124
- if (absorbance === undefined || transmittance === undefined) return [];
125
-
126
- const picked = autoPeakPicking(
127
- measurementOf(wavenumber, absorbance, transmittance),
128
- // Absorbance, because a band is a maximum there and a minimum in the other.
129
- { yVariable: 'a', ...picking },
130
- );
131
-
132
- const bands: IrBand[] = [];
133
- for (const peak of picked) {
134
- if (!Number.isFinite(peak.wavenumber)) continue;
135
- bands.push({
136
- spectrumId: id,
137
- wavenumber: peak.wavenumber,
138
- absorbance: peak.absorbance,
139
- // `ConvertedPeak.transmittance` is a fraction where the spectrum's own
140
- // transmittance is a percentage; brought onto the one scale here so that
141
- // nothing downstream has to remember which of the two it is holding.
142
- transmittance: peak.transmittance * 100,
143
- strength: peak.kind as IrBandStrength,
144
- });
145
- }
146
- return bands;
147
- }
148
-
149
119
  /**
150
120
  * The spectrum as `ir-spectrum` wants it.
151
121
  *
@@ -158,7 +128,7 @@ function infraredBands(
158
128
  * @param transmittance - Percent transmittance at each.
159
129
  * @returns A measurement carrying all three variables.
160
130
  */
161
- function measurementOf(
131
+ export function infraredMeasurement(
162
132
  wavenumber: Float64Array,
163
133
  absorbance: Float64Array,
164
134
  transmittance: Float64Array,
@@ -195,6 +165,43 @@ function measurementOf(
195
165
  };
196
166
  }
197
167
 
168
+ /**
169
+ * The bands of an infrared spectrum, picked on its absorbance.
170
+ * @param spectrum - The spectrum to pick from.
171
+ * @param picking - Where to pick, and how finely.
172
+ * @returns The bands, in the order they were found; none when the spectrum
173
+ * carries only one of its two axes, since the strength is read off the other.
174
+ */
175
+ function infraredBands(
176
+ spectrum: IrSpectrum,
177
+ picking: PeakPickingOptions,
178
+ ): IrBand[] {
179
+ const { id, wavenumber, absorbance, transmittance } = spectrum;
180
+ if (absorbance === undefined || transmittance === undefined) return [];
181
+
182
+ const picked = autoPeakPicking(
183
+ infraredMeasurement(wavenumber, absorbance, transmittance),
184
+ // Absorbance, because a band is a maximum there and a minimum in the other.
185
+ { yVariable: 'a', ...picking },
186
+ );
187
+
188
+ const bands: IrBand[] = [];
189
+ for (const peak of picked) {
190
+ if (!Number.isFinite(peak.wavenumber)) continue;
191
+ bands.push({
192
+ spectrumId: id,
193
+ wavenumber: peak.wavenumber,
194
+ absorbance: peak.absorbance,
195
+ // `ConvertedPeak.transmittance` is a fraction where the spectrum's own
196
+ // transmittance is a percentage; brought onto the one scale here so that
197
+ // nothing downstream has to remember which of the two it is holding.
198
+ transmittance: peak.transmittance * 100,
199
+ strength: peak.kind as IrBandStrength,
200
+ });
201
+ }
202
+ return bands;
203
+ }
204
+
198
205
  /**
199
206
  * Order two bands by height, tallest first.
200
207
  * @param first - One band.
@@ -56,7 +56,7 @@ export function ramanBands(
56
56
  spectrumId: id,
57
57
  wavenumber: peak.wavenumber,
58
58
  intensity: value,
59
- strength: strengthOf(value, min, max),
59
+ strength: intensityStrength(value, min, max),
60
60
  });
61
61
  }
62
62
  return bands;
@@ -69,7 +69,7 @@ export function ramanBands(
69
69
  * @param highest - Its highest.
70
70
  * @returns The letter a correlation table would use.
71
71
  */
72
- function strengthOf(
72
+ export function intensityStrength(
73
73
  value: number,
74
74
  lowest: number,
75
75
  highest: number,
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The bands a spectrum carries.
3
+ *
4
+ * A spectrum arriving without bands is picked once, with the picking settings
5
+ * in force, and from then on its bands are part of it: they travel to a host
6
+ * with it, and a host's own reach the chart. Bands found automatically follow
7
+ * the settings — moving the threshold picks them again — while bands picked by
8
+ * hand, or handed over, are left as they are.
9
+ */
10
+
11
+ import type { IrBand } from './irBand.ts';
12
+ import type { IrSpectrum } from './irSpectrum.ts';
13
+ import type { PickBandsOptions } from './pickBands.ts';
14
+ import { pickBands } from './pickBands.ts';
15
+
16
+ /**
17
+ * A spectrum carrying its bands: picked now when it has none, its own kept and
18
+ * put in order otherwise.
19
+ * @param spectrum - The spectrum, as it arrived.
20
+ * @param picking - How to pick it, if it has to be.
21
+ * @returns The spectrum with its bands; the same object when it had them, in
22
+ * order.
23
+ */
24
+ export function withBands(
25
+ spectrum: IrSpectrum,
26
+ picking: PickBandsOptions,
27
+ ): IrSpectrum {
28
+ const { id, bands } = spectrum;
29
+ if (bands === undefined) {
30
+ return {
31
+ ...spectrum,
32
+ bands: pickBands(spectrum, picking),
33
+ bandsByHand: false,
34
+ };
35
+ }
36
+ const ordered = orderedBands(id, bands);
37
+ return ordered === bands ? spectrum : { ...spectrum, bands: ordered };
38
+ }
39
+
40
+ /**
41
+ * A spectrum's bands picked again with other settings, unless they were picked
42
+ * by hand.
43
+ * @param spectrum - The spectrum.
44
+ * @param picking - The settings now in force.
45
+ * @returns The spectrum re-picked, or the same object when its bands are its
46
+ * own.
47
+ */
48
+ export function repickBands(
49
+ spectrum: IrSpectrum,
50
+ picking: PickBandsOptions,
51
+ ): IrSpectrum {
52
+ if (spectrum.bandsByHand) return spectrum;
53
+ return {
54
+ ...spectrum,
55
+ bands: pickBands(spectrum, picking),
56
+ bandsByHand: false,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Bands as a spectrum holds them: ascending, one per wavenumber, each naming
62
+ * the spectrum, and none at a wavenumber that is not a number.
63
+ * @param spectrumId - The spectrum they belong to.
64
+ * @param bands - The bands, in any order.
65
+ * @returns The bands in order; the same array when they already were.
66
+ */
67
+ export function orderedBands(spectrumId: string, bands: IrBand[]): IrBand[] {
68
+ if (isOrdered(spectrumId, bands)) return bands;
69
+ const kept: IrBand[] = [];
70
+ for (const band of bands.toSorted(byWavenumber)) {
71
+ if (!Number.isFinite(band.wavenumber)) continue;
72
+ if (kept.at(-1)?.wavenumber === band.wavenumber) continue;
73
+ kept.push(band.spectrumId === spectrumId ? band : { ...band, spectrumId });
74
+ }
75
+ return kept;
76
+ }
77
+
78
+ /**
79
+ * Whether two picking settings pick the same bands.
80
+ * @param first - One.
81
+ * @param second - The other.
82
+ * @returns Whether every option agrees.
83
+ */
84
+ export function samePicking(
85
+ first: PickBandsOptions,
86
+ second: PickBandsOptions,
87
+ ): boolean {
88
+ return (
89
+ first === second ||
90
+ (first.from === second.from &&
91
+ first.to === second.to &&
92
+ first.minRelativeHeight === second.minRelativeHeight &&
93
+ first.minPeakWidth === second.minPeakWidth &&
94
+ first.limit === second.limit)
95
+ );
96
+ }
97
+
98
+ function isOrdered(spectrumId: string, bands: readonly IrBand[]): boolean {
99
+ let previous = Number.NEGATIVE_INFINITY;
100
+ for (const band of bands) {
101
+ const { wavenumber } = band;
102
+ if (band.spectrumId !== spectrumId || !Number.isFinite(wavenumber)) {
103
+ return false;
104
+ }
105
+ if (wavenumber <= previous) return false;
106
+ previous = wavenumber;
107
+ }
108
+ return true;
109
+ }
110
+
111
+ function byWavenumber(first: IrBand, second: IrBand): number {
112
+ return first.wavenumber - second.wavenumber;
113
+ }
@@ -1,4 +1,4 @@
1
- import { Icon } from '@blueprintjs/core';
1
+ import { Button, Icon } from '@blueprintjs/core';
2
2
 
3
3
  import {
4
4
  panelCellStyle,
@@ -24,6 +24,11 @@ export interface BandRowProps {
24
24
  * @default undefined
25
25
  */
26
26
  onHighlight?: (band: IrBand | null) => void;
27
+ /**
28
+ * Takes the band away; the row ends with the button that calls it.
29
+ * @default undefined — no button
30
+ */
31
+ onRemove?: (band: IrBand) => void;
27
32
  }
28
33
 
29
34
  /**
@@ -37,7 +42,7 @@ export interface BandRowProps {
37
42
  * @returns The row.
38
43
  */
39
44
  export function BandRow(props: BandRowProps) {
40
- const { entry, mode, assignments, lit, onHighlight } = props;
45
+ const { entry, mode, assignments, lit, onHighlight, onRemove } = props;
41
46
  const { band } = entry;
42
47
 
43
48
  return (
@@ -54,6 +59,17 @@ export function BandRow(props: BandRowProps) {
54
59
  {formatIrValue(bandValue(band, mode) ?? Number.NaN, mode)}
55
60
  </td>
56
61
  {assignments ? <CandidatesCell entry={entry} /> : null}
62
+ {onRemove === undefined ? null : (
63
+ <td style={removeCellStyle}>
64
+ <Button
65
+ variant="minimal"
66
+ size="small"
67
+ icon="small-cross"
68
+ aria-label={`Take away the band at ${formatWavenumber(band.wavenumber)} cm⁻¹`}
69
+ onClick={() => onRemove(band)}
70
+ />
71
+ </td>
72
+ )}
57
73
  </tr>
58
74
  );
59
75
  }
@@ -105,3 +121,5 @@ const STRENGTH_TITLES = {
105
121
  } as const;
106
122
 
107
123
  const litRowStyle = { background: '#fff3e0' } as const;
124
+
125
+ const removeCellStyle = { ...panelCellStyle, padding: 0, width: 1 } as const;
@@ -1,21 +1,19 @@
1
- import { useMemo } from 'react';
2
- import { Button, Toolbar } from 'react-science/ui';
3
-
4
1
  import {
5
2
  panelBodyStyle,
6
3
  panelEmptyStyle,
7
4
  panelHeaderCellStyle,
8
5
  panelStyle,
9
6
  panelTableStyle,
10
- panelToolbarStyle,
11
7
  } from '../../panel/core/panelStyles.ts';
8
+ import { removeBand } from '../core/handPicking.ts';
12
9
  import type { IrBand } from '../core/irBand.ts';
13
10
  import { sameIrBand } from '../core/irBand.ts';
14
11
  import { shortModeLabel } from '../core/irFormat.ts';
12
+ import type { IrSpectrum } from '../core/irSpectrum.ts';
15
13
  import { IR_TECHNIQUE_RULES } from '../core/irTechnique.ts';
16
14
 
17
15
  import { BandRow } from './BandRow.tsx';
18
- import { bandsToTsv } from './bandsTsv.ts';
16
+ import { BandsToolbar } from './BandsToolbar.tsx';
19
17
  import { useIrActions, useIrEditorState } from './irStateContext.ts';
20
18
 
21
19
  export interface BandsPanelProps {
@@ -45,6 +43,11 @@ export interface BandsPanelProps {
45
43
  * row, which is the whole reason the highlight is threaded through the shell
46
44
  * rather than held here: the two views are of one thing.
47
45
  *
46
+ * The list is the spectrum's own and can be edited: a band is taken away from
47
+ * its row, added by a click on the chart while picking by hand, and the whole
48
+ * list found automatically again. Once edited it is the spectrum's as picked
49
+ * by hand, and the threshold no longer moves it.
50
+ *
48
51
  * For a technique the correlation table does not cover — Raman — the column of
49
52
  * candidates and the button naming them are left out, and the table is the
50
53
  * bands themselves.
@@ -55,89 +58,25 @@ export function BandsPanel(props: BandsPanelProps) {
55
58
  const { highlight = null, onHighlight } = props;
56
59
  const { state, selectedSpectrum, assigned } = useIrEditorState();
57
60
  const actions = useIrActions();
58
- const { mode, pickBands, showAssignments, picking, technique } =
59
- state.settings;
61
+ const { mode, pickBands, technique } = state.settings;
60
62
  const { assignments } = IR_TECHNIQUE_RULES[technique];
63
+ const empty = emptyMessage(selectedSpectrum, pickBands, assigned.length);
61
64
 
62
- const rows = useMemo(() => assigned, [assigned]);
63
-
64
- const tsv = useMemo(
65
- () => bandsToTsv(rows, mode, assignments),
66
- [rows, mode, assignments],
67
- );
65
+ function remove(band: IrBand): void {
66
+ if (selectedSpectrum === null) return;
67
+ actions.setBands(
68
+ selectedSpectrum.id,
69
+ removeBand(selectedSpectrum.bands ?? [], band.wavenumber),
70
+ );
71
+ onHighlight?.(null);
72
+ }
68
73
 
69
74
  return (
70
75
  <div style={panelStyle}>
71
- <div style={panelToolbarStyle}>
72
- <Toolbar aria-label="Band actions">
73
- <Toolbar.Item
74
- icon="locate"
75
- active={pickBands}
76
- tooltip={pickBands ? 'Stop picking bands' : 'Pick the bands'}
77
- aria-label="Pick the bands"
78
- onClick={() => actions.setSettings({ pickBands: !pickBands })}
79
- />
80
- {assignments ? (
81
- <Toolbar.Item
82
- icon="label"
83
- active={showAssignments}
84
- tooltip="Name the assignments on the chart"
85
- aria-label="Name the assignments on the chart"
86
- disabled={!pickBands}
87
- onClick={() =>
88
- actions.setSettings({ showAssignments: !showAssignments })
89
- }
90
- />
91
- ) : null}
92
- </Toolbar>
93
-
94
- {/* The threshold is the one picking setting worth reaching for while
95
- reading: everything else about a band follows from the spectrum, and
96
- this is what decides whether a shoulder is a band at all. */}
97
- <label style={thresholdStyle}>
98
- <span style={{ opacity: 0.7 }}>threshold</span>
99
- <input
100
- type="range"
101
- min={0.005}
102
- max={0.3}
103
- step={0.005}
104
- value={picking.minRelativeHeight ?? 0.02}
105
- disabled={!pickBands}
106
- aria-label="How tall a band must be to be picked"
107
- onChange={(event) =>
108
- actions.setSettings({
109
- picking: { minRelativeHeight: event.target.valueAsNumber },
110
- })
111
- }
112
- />
113
- </label>
114
-
115
- <span style={{ flex: 1 }} />
116
-
117
- <Button
118
- variant="minimal"
119
- icon="clipboard"
120
- disabled={rows.length === 0}
121
- tooltipProps={{ content: 'Copy the table' }}
122
- aria-label="Copy the table"
123
- onClick={() => void navigator.clipboard?.writeText(tsv)}
124
- />
125
- </div>
76
+ <BandsToolbar />
126
77
 
127
78
  <div style={panelBodyStyle}>
128
- {selectedSpectrum === null ? (
129
- <p style={panelEmptyStyle}>No spectrum is selected.</p>
130
- ) : !pickBands ? (
131
- <p style={panelEmptyStyle}>
132
- The bands are not being picked. Press the locate button above to
133
- find them.
134
- </p>
135
- ) : rows.length === 0 ? (
136
- <p style={panelEmptyStyle}>
137
- No band was found above the threshold. Lower it to pick the weaker
138
- ones.
139
- </p>
140
- ) : (
79
+ {empty === null ? (
141
80
  <table style={panelTableStyle}>
142
81
  <thead>
143
82
  <tr>
@@ -149,10 +88,11 @@ export function BandsPanel(props: BandsPanelProps) {
149
88
  {assignments ? (
150
89
  <th style={panelHeaderCellStyle}>could be</th>
151
90
  ) : null}
91
+ <th style={panelHeaderCellStyle} />
152
92
  </tr>
153
93
  </thead>
154
94
  <tbody>
155
- {rows.map((entry) => (
95
+ {assigned.map((entry) => (
156
96
  <BandRow
157
97
  key={`${entry.band.spectrumId} ${entry.band.wavenumber}`}
158
98
  entry={entry}
@@ -160,19 +100,37 @@ export function BandsPanel(props: BandsPanelProps) {
160
100
  assignments={assignments}
161
101
  lit={sameIrBand(entry.band, highlight)}
162
102
  onHighlight={onHighlight}
103
+ onRemove={remove}
163
104
  />
164
105
  ))}
165
106
  </tbody>
166
107
  </table>
108
+ ) : (
109
+ <p style={panelEmptyStyle}>{empty}</p>
167
110
  )}
168
111
  </div>
169
112
  </div>
170
113
  );
171
114
  }
172
115
 
173
- const thresholdStyle = {
174
- display: 'inline-flex',
175
- alignItems: 'center',
176
- gap: 4,
177
- fontSize: 11,
178
- } as const;
116
+ /**
117
+ * What the panel says in place of the table, when it has no band to list.
118
+ * @param spectrum - The selected spectrum, `null` for none.
119
+ * @param picking - Whether the bands are picked at all.
120
+ * @param count - How many bands there are to list.
121
+ * @returns The sentence, or `null` when there is a table to show.
122
+ */
123
+ function emptyMessage(
124
+ spectrum: IrSpectrum | null,
125
+ picking: boolean,
126
+ count: number,
127
+ ): string | null {
128
+ if (spectrum === null) return 'No spectrum is selected.';
129
+ if (!picking) {
130
+ return 'The bands are not being picked. Press the locate button above to find them.';
131
+ }
132
+ if (count > 0) return null;
133
+ return spectrum.bandsByHand
134
+ ? 'No band is picked. Click the chart while picking by hand, or find them automatically again.'
135
+ : 'No band was found above the threshold. Lower it to pick the weaker ones.';
136
+ }
@@ -0,0 +1,152 @@
1
+ import { useMemo } from 'react';
2
+ import { Button, Toolbar } from 'react-science/ui';
3
+
4
+ import { panelToolbarStyle } from '../../panel/core/panelStyles.ts';
5
+ import { IR_TECHNIQUE_RULES } from '../core/irTechnique.ts';
6
+
7
+ import { bandsToTsv } from './bandsTsv.ts';
8
+ import { useIrCommandProps } from './irCommandContext.tsx';
9
+ import { useIrActions, useIrEditorState } from './irStateContext.ts';
10
+
11
+ /**
12
+ * The row at the top of the bands panel: the switches — picking at all,
13
+ * picking by hand, naming the assignments — then the threshold, then what
14
+ * acts at once, apart from them on the right: finding the bands automatically
15
+ * again, taking every band away, copying the table.
16
+ * @returns The row.
17
+ */
18
+ export function BandsToolbar() {
19
+ const { state, selectedSpectrum, assigned } = useIrEditorState();
20
+ const actions = useIrActions();
21
+ const command = useIrCommandProps();
22
+ const { tool } = state.view;
23
+ const { mode, pickBands, showAssignments, technique } = state.settings;
24
+ const { assignments } = IR_TECHNIQUE_RULES[technique];
25
+ const byHand = selectedSpectrum?.bandsByHand === true;
26
+
27
+ const tsv = useMemo(
28
+ () => bandsToTsv(assigned, mode, assignments),
29
+ [assigned, mode, assignments],
30
+ );
31
+
32
+ return (
33
+ <div style={panelToolbarStyle}>
34
+ <Toolbar aria-label="Band actions">
35
+ <Toolbar.Item
36
+ icon="locate"
37
+ active={pickBands}
38
+ tooltip={pickBands ? 'Stop picking bands' : 'Pick the bands'}
39
+ aria-label="Pick the bands"
40
+ onClick={() => actions.setSettings({ pickBands: !pickBands })}
41
+ />
42
+ <Toolbar.Item
43
+ icon="add-location"
44
+ active={tool === 'pick'}
45
+ {...command('pickTool')}
46
+ disabled={selectedSpectrum === null}
47
+ />
48
+ {assignments ? (
49
+ <Toolbar.Item
50
+ icon="label"
51
+ active={showAssignments}
52
+ tooltip="Name the assignments on the chart"
53
+ aria-label="Name the assignments on the chart"
54
+ disabled={!pickBands}
55
+ onClick={() =>
56
+ actions.setSettings({ showAssignments: !showAssignments })
57
+ }
58
+ />
59
+ ) : null}
60
+ </Toolbar>
61
+
62
+ <ThresholdSlider />
63
+
64
+ <span style={{ flex: 1 }} />
65
+
66
+ <Button
67
+ variant="minimal"
68
+ icon="refresh"
69
+ disabled={!pickBands || !byHand}
70
+ tooltipProps={{ content: 'Find the bands automatically again' }}
71
+ aria-label="Find the bands automatically again"
72
+ onClick={() => {
73
+ if (selectedSpectrum !== null) {
74
+ actions.resetBands(selectedSpectrum.id);
75
+ }
76
+ }}
77
+ />
78
+ <Button
79
+ variant="minimal"
80
+ icon="eraser"
81
+ disabled={!pickBands || assigned.length === 0}
82
+ tooltipProps={{ content: 'Take every band away, to pick them by hand' }}
83
+ aria-label="Take every band away"
84
+ onClick={() => {
85
+ if (selectedSpectrum !== null) {
86
+ actions.setBands(selectedSpectrum.id, []);
87
+ }
88
+ }}
89
+ />
90
+ <Button
91
+ variant="minimal"
92
+ icon="clipboard"
93
+ disabled={assigned.length === 0}
94
+ tooltipProps={{ content: 'Copy the table' }}
95
+ aria-label="Copy the table"
96
+ onClick={() => void navigator.clipboard?.writeText(tsv)}
97
+ />
98
+ </div>
99
+ );
100
+ }
101
+
102
+ /**
103
+ * The threshold, the one picking setting worth reaching for while reading:
104
+ * everything else about a band follows from the spectrum, and this is what
105
+ * decides whether a shoulder is a band at all. Bands picked by hand are not
106
+ * its to move.
107
+ * @returns The labelled slider.
108
+ */
109
+ function ThresholdSlider() {
110
+ const { state, selectedSpectrum } = useIrEditorState();
111
+ const actions = useIrActions();
112
+ const { pickBands, picking } = state.settings;
113
+ const byHand = selectedSpectrum?.bandsByHand === true;
114
+
115
+ return (
116
+ <label
117
+ style={thresholdStyle}
118
+ title={
119
+ byHand
120
+ ? 'The bands were picked by hand: find them automatically again to use the threshold.'
121
+ : undefined
122
+ }
123
+ >
124
+ <span style={{ opacity: 0.7 }}>threshold</span>
125
+ <input
126
+ type="range"
127
+ min={0.005}
128
+ max={0.3}
129
+ step={0.005}
130
+ style={sliderStyle}
131
+ value={picking.minRelativeHeight ?? 0.02}
132
+ disabled={!pickBands || byHand}
133
+ aria-label="How tall a band must be to be picked"
134
+ onChange={(event) =>
135
+ actions.setSettings({
136
+ picking: { minRelativeHeight: event.target.valueAsNumber },
137
+ })
138
+ }
139
+ />
140
+ </label>
141
+ );
142
+ }
143
+
144
+ const thresholdStyle = {
145
+ display: 'inline-flex',
146
+ alignItems: 'center',
147
+ gap: 4,
148
+ fontSize: 11,
149
+ } as const;
150
+
151
+ /** Narrower than the browser's own, so the row holds every control at 360 px. */
152
+ const sliderStyle = { width: 88 } as const;