@teselagen/ove 0.8.2 → 0.8.4

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 (53) hide show
  1. package/AlignmentView/LabileSitesLayer.d.ts +13 -0
  2. package/AlignmentView/PairwiseAlignmentView.d.ts +1 -9
  3. package/BarPlot/index.d.ts +33 -0
  4. package/LinearView/SequenceName.d.ts +2 -1
  5. package/PropertySidePanel/calculateAminoAcidFrequency.d.ts +46 -0
  6. package/PropertySidePanel/index.d.ts +6 -0
  7. package/RowItem/Caret/index.d.ts +2 -1
  8. package/StatusBar/index.d.ts +2 -1
  9. package/aaprops.svg +2287 -0
  10. package/constants/dnaToColor.d.ts +122 -4
  11. package/index.cjs.js +4214 -7859
  12. package/index.es.js +2166 -5811
  13. package/index.umd.js +3745 -7390
  14. package/ove.css +100 -19
  15. package/package.json +2 -2
  16. package/src/AlignmentView/AlignmentVisibilityTool.js +141 -37
  17. package/src/AlignmentView/LabileSitesLayer.js +33 -0
  18. package/src/AlignmentView/Minimap.js +5 -3
  19. package/src/AlignmentView/PairwiseAlignmentView.js +55 -61
  20. package/src/AlignmentView/index.js +476 -257
  21. package/src/AlignmentView/style.css +27 -0
  22. package/src/BarPlot/index.js +156 -0
  23. package/src/CircularView/Caret.js +8 -2
  24. package/src/CircularView/SelectionLayer.js +4 -2
  25. package/src/CircularView/index.js +5 -1
  26. package/src/Editor/darkmode.css +10 -0
  27. package/src/Editor/index.js +3 -0
  28. package/src/Editor/userDefinedHandlersAndOpts.js +2 -1
  29. package/src/FindBar/index.js +2 -3
  30. package/src/LinearView/SequenceName.js +8 -2
  31. package/src/LinearView/index.js +21 -0
  32. package/src/PropertySidePanel/calculateAminoAcidFrequency.js +77 -0
  33. package/src/PropertySidePanel/index.js +236 -0
  34. package/src/PropertySidePanel/style.css +39 -0
  35. package/src/RowItem/Caret/index.js +8 -2
  36. package/src/RowItem/Labels.js +1 -1
  37. package/src/RowItem/SelectionLayer/index.js +5 -1
  38. package/src/RowItem/Sequence.js +99 -5
  39. package/src/RowItem/Translations/Translation.js +3 -2
  40. package/src/RowItem/Translations/index.js +2 -0
  41. package/src/RowItem/index.js +74 -8
  42. package/src/RowItem/style.css +3 -4
  43. package/src/StatusBar/index.js +11 -4
  44. package/src/constants/dnaToColor.js +151 -0
  45. package/src/helperComponents/PinchHelper/PinchHelper.js +5 -1
  46. package/src/helperComponents/SelectDialog.js +5 -2
  47. package/src/style.css +2 -2
  48. package/src/utils/editorUtils.js +5 -3
  49. package/src/utils/getAlignedAminoAcidSequenceProps.js +379 -0
  50. package/src/withEditorInteractions/createSequenceInputPopup.js +19 -5
  51. package/src/withEditorInteractions/index.js +9 -3
  52. package/utils/editorUtils.d.ts +2 -1
  53. package/utils/getAlignedAminoAcidSequenceProps.d.ts +49 -0
@@ -0,0 +1,379 @@
1
+ function calculatePairwiseIdentity(seq1, seq2, excludeGaps = true) {
2
+ if (seq1.length !== seq2.length) {
3
+ throw new Error("Sequences must be aligned (same length)");
4
+ }
5
+
6
+ let identicalPositions = 0;
7
+ let validPositions = 0;
8
+
9
+ for (let i = 0; i < seq1.length; i++) {
10
+ const aa1 = seq1[i].toUpperCase();
11
+ const aa2 = seq2[i].toUpperCase();
12
+
13
+ // Skip positions with gaps if excludeGaps is true
14
+ if (excludeGaps && aa1 === "-" && aa2 === "-") {
15
+ continue;
16
+ }
17
+
18
+ validPositions++;
19
+
20
+ if (aa1 === aa2) {
21
+ identicalPositions++;
22
+ }
23
+ }
24
+ return { validPositions, identicalPositions };
25
+ }
26
+
27
+ function calculateIdentityMatrix(alignedSequences) {
28
+ const n = Object.keys(alignedSequences).length;
29
+ const identityMatrix = Array(n)
30
+ .fill(n)
31
+ .map(() => Array(n).fill(0));
32
+ const sequenceNames = Object.keys(alignedSequences);
33
+ const _identicalPositions = [];
34
+
35
+ // Calculate pairwise identities
36
+ for (let i = 0; i < n; i++) {
37
+ for (let j = i; j < n; j++) {
38
+ if (i === j) {
39
+ identityMatrix[i][j] = 100.0; // Self-identity
40
+ } else {
41
+ const seq1 = alignedSequences[sequenceNames[i]];
42
+ const seq2 = alignedSequences[sequenceNames[j]];
43
+ const { validPositions, identicalPositions } =
44
+ calculatePairwiseIdentity(seq1, seq2, true);
45
+
46
+ const identityPercentage =
47
+ validPositions > 0 ? (identicalPositions / validPositions) * 100 : 0;
48
+
49
+ _identicalPositions.push({
50
+ identicalPositions,
51
+ seqs: [sequenceNames[i], sequenceNames[j]]
52
+ });
53
+
54
+ identityMatrix[i][j] = identityPercentage;
55
+ identityMatrix[j][i] = identityPercentage;
56
+ }
57
+ }
58
+ }
59
+
60
+ return {
61
+ matrix: identityMatrix,
62
+ sequenceNames: sequenceNames,
63
+ identicalPositions: _identicalPositions
64
+ };
65
+ }
66
+
67
+ function getPropertyAnalysis(alignedSequences) {
68
+ const sequences = Object.values(alignedSequences).slice(1);
69
+
70
+ function getResidueProperties(residue, map) {
71
+ return Object.keys(map).filter(key => map[key].includes(residue));
72
+ }
73
+
74
+ // 4. Intersection helper
75
+ function intersectHelper(arrays) {
76
+ if (arrays.length === 0) return [];
77
+ return arrays.reduce((a, b) => a.filter(x => b.includes(x)));
78
+ }
79
+
80
+ const seqLength = sequences[0].length;
81
+
82
+ return Array.from({ length: seqLength }, (_, pos) => {
83
+ const residues = sequences.map(seq => seq[pos]).filter(r => r !== "-");
84
+
85
+ const sizeProps = residues.map(r =>
86
+ getResidueProperties(r, residueSizeMap)
87
+ );
88
+ const polarityProps = residues.map(r =>
89
+ getResidueProperties(r, polarityMap)
90
+ );
91
+ const specificGroupProps = residues.map(r =>
92
+ getResidueProperties(r, specificGroupMap)
93
+ );
94
+
95
+ const shared = {
96
+ size: intersectHelper(sizeProps),
97
+ polarity: intersectHelper(polarityProps),
98
+ specificGroup: intersectHelper(specificGroupProps)
99
+ };
100
+
101
+ const group =
102
+ shared.specificGroup.length > 0
103
+ ? shared.specificGroup[0]
104
+ : `${shared.size.length ? shared.size[0] : ""} ${shared.polarity.length ? shared.polarity[0] : ""}`;
105
+
106
+ function mostFrequent(arr) {
107
+ const freq = {};
108
+ arr.forEach(val => {
109
+ if (val !== "none") freq[val] = (freq[val] || 0) + 1;
110
+ });
111
+ const max = Math.max(...Object.values(freq), 0);
112
+ const mostFrequentProps = Object.entries(freq)
113
+ .filter(([, count]) => count === max)
114
+ .map(([prop]) => prop);
115
+ return {
116
+ props: mostFrequentProps,
117
+ count: max
118
+ };
119
+ }
120
+
121
+ let mostFreqGroup;
122
+ if (group.trim() === "") {
123
+ const mostFreqResidueGroups = {
124
+ size: mostFrequent(sizeProps.flat()),
125
+ polarity: mostFrequent(polarityProps.flat()),
126
+ specificGroup: mostFrequent(specificGroupProps.flat())
127
+ };
128
+
129
+ const sortedGroups = Object.entries(mostFreqResidueGroups).sort(
130
+ (a, b) => b[1].count - a[1].count
131
+ );
132
+ const topCount = sortedGroups[0][1].count;
133
+ const topOrTiedGroups = sortedGroups.filter(
134
+ ([, val]) => val.count === topCount
135
+ );
136
+
137
+ const specificGroupEntry = topOrTiedGroups.find(
138
+ ([key]) => key === "specificGroup"
139
+ );
140
+
141
+ if (specificGroupEntry) {
142
+ mostFreqGroup = specificGroupEntry[1].props.join(" ");
143
+ } else {
144
+ const allProps = [
145
+ ...new Set(topOrTiedGroups.flatMap(([, val]) => val.props))
146
+ ];
147
+ mostFreqGroup = allProps.join(" ");
148
+ }
149
+ }
150
+
151
+ const aaGroup = group.trim() || mostFreqGroup.trim();
152
+
153
+ return {
154
+ position: pos,
155
+ residues,
156
+ group: aaGroup,
157
+ color: combineGroupColors(aaGroup.split(" "))
158
+ };
159
+ });
160
+ }
161
+
162
+ function getIdentityAndFrequencies(alignedSequences) {
163
+ const sequences = Object.values(alignedSequences).slice(1); // Exclude consensus sequence
164
+ const alignmentLength = sequences[0].length;
165
+
166
+ let totalScore = 0;
167
+ let totalPositions = 0;
168
+ const identityPercentages = [];
169
+ const propertyFrequencies = [];
170
+
171
+ for (let pos = 0; pos < alignmentLength; pos++) {
172
+ const column = sequences.map(seq => seq[pos]);
173
+ const nonGapResidues = column;
174
+ const propertyCounts = {};
175
+
176
+ if (nonGapResidues.length === 0) identityPercentages.push(0);
177
+ if (nonGapResidues.length < 2) continue; // Skip if <2 sequences have residues
178
+
179
+ // Calculate conservation score for this position
180
+ let totalProperties = 0;
181
+ const residueCounts = {};
182
+ nonGapResidues.forEach(aa => {
183
+ residueCounts[aa] = (residueCounts[aa] || 0) + 1;
184
+
185
+ const props = residuePropertyMap[aa] || [];
186
+
187
+ props.forEach(prop => {
188
+ propertyCounts[prop] = (propertyCounts[prop] || 0) + 1;
189
+ });
190
+
191
+ totalProperties++;
192
+ });
193
+
194
+ const propertyPercentages = {};
195
+ Object.entries(propertyCounts).forEach(([prop, count]) => {
196
+ propertyPercentages[prop] = (count / totalProperties) * 100;
197
+ });
198
+
199
+ const maxCount = Math.max(...Object.values(residueCounts));
200
+ const positionScore = maxCount / nonGapResidues.length;
201
+ identityPercentages.push((maxCount / nonGapResidues.length) * 100);
202
+ propertyFrequencies.push(propertyPercentages);
203
+
204
+ totalScore += positionScore;
205
+ totalPositions++;
206
+ }
207
+
208
+ const overallIdentity =
209
+ totalPositions > 0 ? (totalScore / totalPositions) * 100 : 0;
210
+
211
+ return {
212
+ overallIdentity,
213
+ frequencies: identityPercentages
214
+ };
215
+ }
216
+
217
+ function getLabileSites(alignedSequences, threshold = 0.5) {
218
+ const sequences = Object.values(alignedSequences);
219
+ const alignmentLength = sequences[0].length;
220
+
221
+ const labileSites = [];
222
+ const conservationScores = [];
223
+
224
+ for (let pos = 0; pos < alignmentLength; pos++) {
225
+ const column = sequences.map(seq => seq[pos]);
226
+ const nonGapResidues = column.filter(aa => aa !== "-");
227
+
228
+ if (nonGapResidues.length < 2) {
229
+ conservationScores.push(null); // Skip gap-only columns
230
+ continue;
231
+ }
232
+
233
+ // Count residue frequencies
234
+ const counts = {};
235
+ nonGapResidues.forEach(aa => {
236
+ counts[aa] = (counts[aa] || 0) + 1;
237
+ });
238
+
239
+ // Calculate conservation score (0 = completely variable, 1 = completely conserved)
240
+ const maxCount = Math.max(...Object.values(counts));
241
+ const conservationScore = maxCount / nonGapResidues.length;
242
+
243
+ conservationScores.push(conservationScore);
244
+
245
+ // Identify labile sites (low conservation)
246
+ if (conservationScore <= threshold) {
247
+ labileSites.push({
248
+ position: pos + 1, // 1-based indexing
249
+ conservationScore: conservationScore,
250
+ residueVariation: Object.keys(counts),
251
+ frequencies: counts
252
+ });
253
+ }
254
+ }
255
+
256
+ return {
257
+ sites: labileSites,
258
+ conservationScores: conservationScores,
259
+ totalLabileSites: labileSites.length,
260
+ percentageLabile: (labileSites.length / alignmentLength) * 100
261
+ };
262
+ }
263
+
264
+ export const getAlignedAminoAcidSequenceProps = tracks => {
265
+ let sequences = {};
266
+
267
+ tracks.forEach(at => {
268
+ sequences = {
269
+ ...sequences,
270
+ [at.alignmentData.name]: at.alignmentData.sequence
271
+ };
272
+ });
273
+
274
+ const identity = calculateIdentityMatrix(sequences);
275
+ const { overallIdentity, frequencies } = getIdentityAndFrequencies(sequences);
276
+ const labileSites = getLabileSites(sequences, 0.5);
277
+ const propertyAnalysis = getPropertyAnalysis(sequences);
278
+
279
+ return {
280
+ ...identity,
281
+ overallIdentity,
282
+ frequencies,
283
+ labileSites,
284
+ propertyAnalysis
285
+ };
286
+ };
287
+
288
+ const residueSizeMap = {
289
+ tiny: ["A", "C", "G", "S"],
290
+ small: ["A", "C", "D", "G", "N", "P", "S", "T", "V"],
291
+ large: ["E", "F", "H", "I", "K", "L", "M", "Q", "R", "W", "Y"]
292
+ };
293
+
294
+ const polarityMap = {
295
+ hydrophobic: ["A", "C", "F", "I", "L", "M", "V", "W", "Y", "H", "K", "R"],
296
+ polar: ["D", "E", "H", "K", "N", "Q", "R", "S", "T", "Y"],
297
+ charged: ["D", "E", "H", "K", "R"]
298
+ };
299
+
300
+ const specificGroupMap = {
301
+ aliphatic: ["I", "L", "V"],
302
+ aromatic: ["F", "W", "Y", "H"],
303
+ positive: ["H", "K", "R"],
304
+ negative: ["D", "E"],
305
+ amidic: ["N", "Q"],
306
+ "sulphur-containing": ["C", "M"],
307
+ hydroxylic: ["S", "T"]
308
+ };
309
+
310
+ export const residuePropertyMap = {
311
+ A: ["Small", "Tiny", "Hydrophobic"],
312
+ C: ["Small", "Tiny", "Hydrophobic", "Sulphur-Containing"],
313
+ D: ["Small", "Polar", "Charged", "Negative"],
314
+ E: ["Large", "Polar", "Charged", "Negative"],
315
+ F: ["Hydrophobic", "Aromatic"],
316
+ G: ["Small", "Tiny"],
317
+ H: ["Hydrophobic", "Polar", "Charged", "Aromatic", "Positive"],
318
+ I: ["Hydrophobic", "Aliphatic"],
319
+ K: ["Hydrophobic", "Polar", "Charged", "Positive"],
320
+ L: ["Hydrophobic", "Aliphatic"],
321
+ M: ["Hydrophobic", "Sulphur-Containing"],
322
+ N: ["Polar", "Small", "Amidic"],
323
+ P: ["Small"],
324
+ Q: ["Polar", "Amidic"],
325
+ R: ["Polar", "Charged", "Positive"],
326
+ S: ["Polar", "Small", "Tiny", "Hydroxylic"],
327
+ T: ["Polar", "Small", "Hydroxylic"],
328
+ V: ["Hydrophobic", "Small", "Aliphatic"],
329
+ W: ["Hydrophobic", "Polar", "Aromatic"],
330
+ Y: ["Hydrophobic", "Polar", "Aromatic"]
331
+ };
332
+
333
+ const propertiesColorMap = {
334
+ aliphatic: "#AE83A3",
335
+ aromatic: "#EC8BA0",
336
+ amidic: "#83C6C2",
337
+ hydroxylic: "#65A3AC",
338
+ "sulphur-containing": "#F8CD7F",
339
+ positive: "#A1838F",
340
+ negative: "#DC855C",
341
+ large: "#C1B87E",
342
+ small: "#B1DEF0",
343
+ tiny: "#74BDA8",
344
+ hydrophobic: "#F4B3A2",
345
+ polar: "#C1DCAE",
346
+ charged: "#D7AD7A",
347
+ none: "#888"
348
+ };
349
+
350
+ function combineGroupColors(colorKeys, colorMap = propertiesColorMap) {
351
+ if (!colorKeys.length || colorKeys[0] === "none") return "#000";
352
+
353
+ const toRGB = hex => {
354
+ if (!hex) return [0, 0, 0];
355
+ const h = hex.replace("#", "");
356
+ return [
357
+ parseInt(h.substring(0, 2), 16),
358
+ parseInt(h.substring(2, 4), 16),
359
+ parseInt(h.substring(4, 6), 16)
360
+ ];
361
+ };
362
+
363
+ const rgbs = colorKeys
364
+ .map(key => toRGB(colorMap[key]))
365
+ .filter(rgb => rgb.every(Number.isFinite));
366
+ if (rgbs.length === 0) return "#888";
367
+
368
+ const avg = [0, 1, 2].map(i =>
369
+ Math.round(rgbs.reduce((sum, rgb) => sum + rgb[i], 0) / rgbs.length)
370
+ );
371
+
372
+ return (
373
+ "#" +
374
+ avg
375
+ .map(val => val.toString(16).padStart(2, "0"))
376
+ .join("")
377
+ .toUpperCase()
378
+ );
379
+ }
@@ -81,7 +81,8 @@ class SequenceInputNoHotkeys extends React.Component {
81
81
  isProtein,
82
82
  caretPosition,
83
83
  sequenceData,
84
- maxInsertSize
84
+ maxInsertSize,
85
+ showAminoAcidUnitAsCodon
85
86
  } = this.props;
86
87
  const { charsToInsert, hasTempError } = this.state;
87
88
 
@@ -97,7 +98,12 @@ class SequenceInputNoHotkeys extends React.Component {
97
98
  <span>
98
99
  Press <span style={{ fontWeight: "bolder" }}>ENTER</span> to replace{" "}
99
100
  {divideBy3(getRangeLength(selectionLayer, sequenceLength), isProtein)}{" "}
100
- {isProtein ? "AAs" : "base pairs"} between{" "}
101
+ {isProtein
102
+ ? showAminoAcidUnitAsCodon
103
+ ? "codons"
104
+ : "AAs"
105
+ : "base pairs"}{" "}
106
+ between{" "}
101
107
  {isProtein
102
108
  ? convertDnaCaretPositionOrRangeToAA(betweenVals[0])
103
109
  : betweenVals[0]}{" "}
@@ -111,8 +117,12 @@ class SequenceInputNoHotkeys extends React.Component {
111
117
  message = (
112
118
  <span>
113
119
  Press <span style={{ fontWeight: "bolder" }}>ENTER</span> to insert{" "}
114
- {charsToInsert.length} {isProtein ? "AAs" : "base pairs"} after{" "}
115
- {isProtein ? "AA" : "base"}{" "}
120
+ {charsToInsert.length}{" "}
121
+ {isProtein
122
+ ? `${showAminoAcidUnitAsCodon ? "codons" : "AAs"}`
123
+ : "base pairs"}{" "}
124
+ after{" "}
125
+ {isProtein ? `${showAminoAcidUnitAsCodon ? "codon" : "AA"}` : "base"}{" "}
116
126
  {isProtein
117
127
  ? convertDnaCaretPositionOrRangeToAA(caretPosition)
118
128
  : caretPosition}
@@ -156,7 +166,7 @@ class SequenceInputNoHotkeys extends React.Component {
156
166
  }
157
167
  if (maxInsertSize && sanitizedVal.length > maxInsertSize) {
158
168
  return window.toastr.error(
159
- `Sorry, your insert is greater than ${maxInsertSize}`,
169
+ `Sorry, your insert is greater than ${maxInsertSize}`
160
170
  );
161
171
  }
162
172
  e.target.value = sanitizedVal;
@@ -221,6 +231,10 @@ export default function createSequenceInputPopup(props) {
221
231
  // function closeInput() {
222
232
  // sequenceInputBubble.remove();
223
233
  // }
234
+ if (document.getElementById("sequenceInputBubble")) {
235
+ // remove the old one if it exists
236
+ document.getElementById("sequenceInputBubble").outerHTML = "";
237
+ }
224
238
  div = document.createElement("div");
225
239
  div.style.zIndex = "400000";
226
240
  div.id = "sequenceInputBubble";
@@ -238,8 +238,12 @@ function VectorInteractionHOC(Component /* options */) {
238
238
  sequence: clipboardData.getData("text/plain") || e.target.value
239
239
  };
240
240
  }
241
- if (sequenceData.isProtein && !seqDataToInsert.proteinSequence) {
242
- seqDataToInsert.proteinSequence = seqDataToInsert.sequence;
241
+ if (sequenceData.isProtein) {
242
+ seqDataToInsert.isProtein = true;
243
+
244
+ if (!seqDataToInsert.proteinSequence) {
245
+ seqDataToInsert.proteinSequence = seqDataToInsert.sequence;
246
+ }
243
247
  }
244
248
 
245
249
  if (
@@ -399,7 +403,8 @@ function VectorInteractionHOC(Component /* options */) {
399
403
  sequenceData = { sequence: "" },
400
404
  readOnly,
401
405
  disableBpEditing,
402
- maxInsertSize
406
+ maxInsertSize,
407
+ showAminoAcidUnitAsCodon
403
408
  // updateSequenceData,
404
409
  // wrappedInsertSequenceDataAtPositionOrRange
405
410
  // handleInsert
@@ -421,6 +426,7 @@ function VectorInteractionHOC(Component /* options */) {
421
426
  sequenceLength,
422
427
  caretPosition,
423
428
  maxInsertSize,
429
+ showAminoAcidUnitAsCodon,
424
430
  handleInsert: async seqDataToInsert => {
425
431
  await insertAndSelectHelper({
426
432
  props: this.props,
@@ -1,4 +1,4 @@
1
- export function getSelectionMessage({ caretPosition, selectionLayer, customTitle, sequenceLength, sequenceData, showGCContent, GCDecimalDigits, isProtein }: {
1
+ export function getSelectionMessage({ caretPosition, selectionLayer, customTitle, sequenceLength, sequenceData, showGCContent, GCDecimalDigits, isProtein, showAminoAcidUnitAsCodon }: {
2
2
  caretPosition?: number | undefined;
3
3
  selectionLayer?: {
4
4
  start: number;
@@ -10,6 +10,7 @@ export function getSelectionMessage({ caretPosition, selectionLayer, customTitle
10
10
  showGCContent: any;
11
11
  GCDecimalDigits: any;
12
12
  isProtein: any;
13
+ showAminoAcidUnitAsCodon: any;
13
14
  }): string;
14
15
  export function preventDefaultStopPropagation(e: any): void;
15
16
  export function getNodeToRefocus(caretEl: any): any;
@@ -0,0 +1,49 @@
1
+ export function getAlignedAminoAcidSequenceProps(tracks: any): {
2
+ overallIdentity: number;
3
+ frequencies: number[];
4
+ labileSites: {
5
+ sites: {
6
+ position: number;
7
+ conservationScore: number;
8
+ residueVariation: string[];
9
+ frequencies: {};
10
+ }[];
11
+ conservationScores: (number | null)[];
12
+ totalLabileSites: number;
13
+ percentageLabile: number;
14
+ };
15
+ propertyAnalysis: {
16
+ position: number;
17
+ residues: any[];
18
+ group: any;
19
+ color: string;
20
+ }[];
21
+ matrix: any[][];
22
+ sequenceNames: string[];
23
+ identicalPositions: {
24
+ identicalPositions: number;
25
+ seqs: string[];
26
+ }[];
27
+ };
28
+ export namespace residuePropertyMap {
29
+ let A: string[];
30
+ let C: string[];
31
+ let D: string[];
32
+ let E: string[];
33
+ let F: string[];
34
+ let G: string[];
35
+ let H: string[];
36
+ let I: string[];
37
+ let K: string[];
38
+ let L: string[];
39
+ let M: string[];
40
+ let N: string[];
41
+ let P: string[];
42
+ let Q: string[];
43
+ let R: string[];
44
+ let S: string[];
45
+ let T: string[];
46
+ let V: string[];
47
+ let W: string[];
48
+ let Y: string[];
49
+ }