react-msaview 4.6.0 → 4.8.1

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 (93) hide show
  1. package/bundle/index.js +99 -99
  2. package/bundle/index.js.LICENSE.txt +6 -6
  3. package/bundle/index.js.map +1 -1
  4. package/dist/__snapshots__/parseAsn1.test.js.snap +2400 -0
  5. package/dist/components/header/HeaderInfoArea.js +3 -4
  6. package/dist/components/header/HeaderInfoArea.js.map +1 -1
  7. package/dist/components/import/ImportForm.js +6 -2
  8. package/dist/components/import/ImportForm.js.map +1 -1
  9. package/dist/components/import/util.d.ts +1 -1
  10. package/dist/components/import/util.js +4 -1
  11. package/dist/components/import/util.js.map +1 -1
  12. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +7 -2
  13. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  14. package/dist/components/msa/renderMSABlock.js.map +1 -1
  15. package/dist/components/tree/renderTreeCanvas.js +10 -8
  16. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  17. package/dist/model.d.ts +153 -16
  18. package/dist/model.js +97 -29
  19. package/dist/model.js.map +1 -1
  20. package/dist/rowCoordinateCalculations.d.ts +69 -9
  21. package/dist/rowCoordinateCalculations.js +118 -46
  22. package/dist/rowCoordinateCalculations.js.map +1 -1
  23. package/dist/rowCoordinateCalculations.test.js +152 -52
  24. package/dist/rowCoordinateCalculations.test.js.map +1 -1
  25. package/dist/seqPosToGlobalCol.d.ts +19 -0
  26. package/dist/seqPosToGlobalCol.js +34 -0
  27. package/dist/seqPosToGlobalCol.js.map +1 -0
  28. package/dist/seqPosToGlobalCol.test.js +60 -0
  29. package/dist/seqPosToGlobalCol.test.js.map +1 -0
  30. package/dist/util.d.ts +1 -2
  31. package/dist/util.js +0 -9
  32. package/dist/util.js.map +1 -1
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/package.json +7 -9
  36. package/src/components/header/HeaderInfoArea.tsx +2 -5
  37. package/src/components/import/ImportForm.tsx +6 -1
  38. package/src/components/import/util.ts +4 -0
  39. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +7 -2
  40. package/src/components/msa/renderMSABlock.ts +5 -1
  41. package/src/components/tree/renderTreeCanvas.ts +11 -9
  42. package/src/declare.d.ts +0 -1
  43. package/src/model.ts +122 -42
  44. package/src/rowCoordinateCalculations.test.ts +167 -74
  45. package/src/rowCoordinateCalculations.ts +138 -63
  46. package/src/seqPosToGlobalCol.test.ts +71 -0
  47. package/src/seqPosToGlobalCol.ts +40 -0
  48. package/src/util.ts +1 -19
  49. package/src/version.ts +1 -1
  50. package/dist/parseGFF.d.ts +0 -10
  51. package/dist/parseGFF.js +0 -31
  52. package/dist/parseGFF.js.map +0 -1
  53. package/dist/parseNewick.d.ts +0 -60
  54. package/dist/parseNewick.js +0 -95
  55. package/dist/parseNewick.js.map +0 -1
  56. package/dist/parsers/A3mMSA.d.ts +0 -43
  57. package/dist/parsers/A3mMSA.js +0 -277
  58. package/dist/parsers/A3mMSA.js.map +0 -1
  59. package/dist/parsers/A3mMSA.test.js +0 -138
  60. package/dist/parsers/A3mMSA.test.js.map +0 -1
  61. package/dist/parsers/ClustalMSA.d.ts +0 -30
  62. package/dist/parsers/ClustalMSA.js +0 -55
  63. package/dist/parsers/ClustalMSA.js.map +0 -1
  64. package/dist/parsers/EmfMSA.d.ts +0 -27
  65. package/dist/parsers/EmfMSA.js +0 -53
  66. package/dist/parsers/EmfMSA.js.map +0 -1
  67. package/dist/parsers/EmfTree.d.ts +0 -5
  68. package/dist/parsers/EmfTree.js +0 -8
  69. package/dist/parsers/EmfTree.js.map +0 -1
  70. package/dist/parsers/FastaMSA.d.ts +0 -19
  71. package/dist/parsers/FastaMSA.js +0 -69
  72. package/dist/parsers/FastaMSA.js.map +0 -1
  73. package/dist/parsers/StockholmMSA.d.ts +0 -68
  74. package/dist/parsers/StockholmMSA.js +0 -107
  75. package/dist/parsers/StockholmMSA.js.map +0 -1
  76. package/dist/seqCoordToRowSpecificGlobalCoord.d.ts +0 -4
  77. package/dist/seqCoordToRowSpecificGlobalCoord.js +0 -19
  78. package/dist/seqCoordToRowSpecificGlobalCoord.js.map +0 -1
  79. package/dist/seqCoordToRowSpecificGlobalCoord.test.d.ts +0 -1
  80. package/dist/seqCoordToRowSpecificGlobalCoord.test.js +0 -42
  81. package/dist/seqCoordToRowSpecificGlobalCoord.test.js.map +0 -1
  82. package/src/parseGFF.ts +0 -34
  83. package/src/parseNewick.ts +0 -94
  84. package/src/parsers/A3mMSA.test.ts +0 -164
  85. package/src/parsers/A3mMSA.ts +0 -321
  86. package/src/parsers/ClustalMSA.ts +0 -69
  87. package/src/parsers/EmfMSA.ts +0 -67
  88. package/src/parsers/EmfTree.ts +0 -9
  89. package/src/parsers/FastaMSA.ts +0 -82
  90. package/src/parsers/StockholmMSA.ts +0 -140
  91. package/src/seqCoordToRowSpecificGlobalCoord.test.ts +0 -53
  92. package/src/seqCoordToRowSpecificGlobalCoord.ts +0 -25
  93. /package/dist/{parsers/A3mMSA.test.d.ts → seqPosToGlobalCol.test.d.ts} +0 -0
@@ -1,12 +1,14 @@
1
1
  import { expect, test } from 'vitest'
2
2
 
3
3
  import {
4
- globalCoordToRowSpecificCoord,
5
- mouseOverCoordToGapRemovedCoord,
6
- mouseOverCoordToGlobalCoord,
4
+ globalColToSeqPos,
5
+ globalColToVisibleCol,
6
+ visibleColToGlobalCol,
7
+ visibleColToSeqPos,
7
8
  } from './rowCoordinateCalculations'
8
9
 
9
- test('with blanks at positions [2, 5, 8]', () => {
10
+ // Tests for visibleColToGlobalCol (visible global)
11
+ test('visibleColToGlobalCol with blanks at positions [2, 5, 8]', () => {
10
12
  const blanks = [2, 5, 8]
11
13
  ;(
12
14
  [
@@ -20,11 +22,11 @@ test('with blanks at positions [2, 5, 8]', () => {
20
22
  [7, 10],
21
23
  ] as const
22
24
  ).forEach(r => {
23
- expect(mouseOverCoordToGlobalCoord(blanks, r[0])).toBe(r[1])
25
+ expect(visibleColToGlobalCol(blanks, r[0])).toBe(r[1])
24
26
  })
25
27
  })
26
28
 
27
- test('with no blanks', () => {
29
+ test('visibleColToGlobalCol with no blanks', () => {
28
30
  const blanks: number[] = []
29
31
  ;(
30
32
  [
@@ -34,11 +36,11 @@ test('with no blanks', () => {
34
36
  [10, 10],
35
37
  ] as const
36
38
  ).forEach(r => {
37
- expect(mouseOverCoordToGlobalCoord(blanks, r[0])).toBe(r[1])
39
+ expect(visibleColToGlobalCol(blanks, r[0])).toBe(r[1])
38
40
  })
39
41
  })
40
42
 
41
- test('with consecutive blanks', () => {
43
+ test('visibleColToGlobalCol with consecutive blanks', () => {
42
44
  const blanks = [2, 3, 4, 7, 8]
43
45
  ;(
44
46
  [
@@ -50,11 +52,11 @@ test('with consecutive blanks', () => {
50
52
  [5, 10],
51
53
  ] as const
52
54
  ).forEach(r => {
53
- expect(mouseOverCoordToGlobalCoord(blanks, r[0])).toBe(r[1])
55
+ expect(visibleColToGlobalCol(blanks, r[0])).toBe(r[1])
54
56
  })
55
57
  })
56
58
 
57
- test('with blanks at the beginning', () => {
59
+ test('visibleColToGlobalCol with blanks at the beginning', () => {
58
60
  const blanks = [1, 2, 5]
59
61
  ;(
60
62
  [
@@ -65,11 +67,11 @@ test('with blanks at the beginning', () => {
65
67
  [4, 7],
66
68
  ] as const
67
69
  ).forEach(r => {
68
- expect(mouseOverCoordToGlobalCoord(blanks, r[0])).toBe(r[1])
70
+ expect(visibleColToGlobalCol(blanks, r[0])).toBe(r[1])
69
71
  })
70
72
  })
71
73
 
72
- test('with position exceeding blanks array', () => {
74
+ test('visibleColToGlobalCol with position exceeding blanks array', () => {
73
75
  const blanks = [2, 5]
74
76
  ;(
75
77
  [
@@ -81,89 +83,180 @@ test('with position exceeding blanks array', () => {
81
83
  [10, 12], // Far beyond blanks array
82
84
  ] as const
83
85
  ).forEach(r => {
84
- expect(mouseOverCoordToGlobalCoord(blanks, r[0])).toBe(r[1])
86
+ expect(visibleColToGlobalCol(blanks, r[0])).toBe(r[1])
85
87
  })
86
88
  })
87
89
 
88
- test('with gaps in sequence', () => {
90
+ // Tests for globalColToVisibleCol (global visible)
91
+ test('globalColToVisibleCol with blanks at positions [2, 5, 8]', () => {
92
+ const blanks = [2, 5, 8]
93
+ // Inverse of visibleColToGlobalCol
94
+ expect(globalColToVisibleCol(blanks, 0)).toBe(0)
95
+ expect(globalColToVisibleCol(blanks, 1)).toBe(1)
96
+ expect(globalColToVisibleCol(blanks, 2)).toBe(undefined) // Hidden
97
+ expect(globalColToVisibleCol(blanks, 3)).toBe(2)
98
+ expect(globalColToVisibleCol(blanks, 4)).toBe(3)
99
+ expect(globalColToVisibleCol(blanks, 5)).toBe(undefined) // Hidden
100
+ expect(globalColToVisibleCol(blanks, 6)).toBe(4)
101
+ expect(globalColToVisibleCol(blanks, 7)).toBe(5)
102
+ expect(globalColToVisibleCol(blanks, 8)).toBe(undefined) // Hidden
103
+ expect(globalColToVisibleCol(blanks, 9)).toBe(6)
104
+ expect(globalColToVisibleCol(blanks, 10)).toBe(7)
105
+ })
106
+
107
+ test('globalColToVisibleCol with no blanks', () => {
108
+ const blanks: number[] = []
109
+ expect(globalColToVisibleCol(blanks, 0)).toBe(0)
110
+ expect(globalColToVisibleCol(blanks, 5)).toBe(5)
111
+ expect(globalColToVisibleCol(blanks, 10)).toBe(10)
112
+ })
113
+
114
+ test('globalColToVisibleCol with consecutive blanks', () => {
115
+ const blanks = [2, 3, 4, 7, 8]
116
+ expect(globalColToVisibleCol(blanks, 0)).toBe(0)
117
+ expect(globalColToVisibleCol(blanks, 1)).toBe(1)
118
+ expect(globalColToVisibleCol(blanks, 2)).toBe(undefined) // Hidden
119
+ expect(globalColToVisibleCol(blanks, 3)).toBe(undefined) // Hidden
120
+ expect(globalColToVisibleCol(blanks, 4)).toBe(undefined) // Hidden
121
+ expect(globalColToVisibleCol(blanks, 5)).toBe(2)
122
+ expect(globalColToVisibleCol(blanks, 6)).toBe(3)
123
+ expect(globalColToVisibleCol(blanks, 7)).toBe(undefined) // Hidden
124
+ expect(globalColToVisibleCol(blanks, 8)).toBe(undefined) // Hidden
125
+ expect(globalColToVisibleCol(blanks, 9)).toBe(4)
126
+ expect(globalColToVisibleCol(blanks, 10)).toBe(5)
127
+ })
128
+
129
+ // Tests for globalColToSeqPos (global column → sequence position)
130
+ test('globalColToSeqPos with gaps in sequence', () => {
89
131
  const sequence = 'AC-GT-A'
90
- expect(globalCoordToRowSpecificCoord(sequence, 0)).toBe(0)
91
- expect(globalCoordToRowSpecificCoord(sequence, 1)).toBe(1)
92
- expect(globalCoordToRowSpecificCoord(sequence, 2)).toBe(2)
132
+ expect(globalColToSeqPos(sequence, 0)).toBe(0)
133
+ expect(globalColToSeqPos(sequence, 1)).toBe(1)
134
+ expect(globalColToSeqPos(sequence, 2)).toBe(2)
93
135
  // Position 2 is a gap, so count before it is 2
94
- expect(globalCoordToRowSpecificCoord(sequence, 3)).toBe(2)
95
- expect(globalCoordToRowSpecificCoord(sequence, 4)).toBe(3)
96
- expect(globalCoordToRowSpecificCoord(sequence, 5)).toBe(4)
136
+ expect(globalColToSeqPos(sequence, 3)).toBe(2)
137
+ expect(globalColToSeqPos(sequence, 4)).toBe(3)
138
+ expect(globalColToSeqPos(sequence, 5)).toBe(4)
97
139
  // Position 5 is a gap, so count before it is 4
98
- expect(globalCoordToRowSpecificCoord(sequence, 6)).toBe(4)
140
+ expect(globalColToSeqPos(sequence, 6)).toBe(4)
99
141
  })
100
142
 
101
- test('with mixed gap characters (- and .)', () => {
143
+ test('globalColToSeqPos with mixed gap characters (- and .)', () => {
102
144
  const sequence = 'AC.GT-A'
103
- expect(globalCoordToRowSpecificCoord(sequence, 0)).toBe(0)
104
- expect(globalCoordToRowSpecificCoord(sequence, 1)).toBe(1)
145
+ expect(globalColToSeqPos(sequence, 0)).toBe(0)
146
+ expect(globalColToSeqPos(sequence, 1)).toBe(1)
105
147
  // Position 2 is a gap (.), so count before it is 2
106
- expect(globalCoordToRowSpecificCoord(sequence, 2)).toBe(2)
107
- expect(globalCoordToRowSpecificCoord(sequence, 3)).toBe(2)
108
- expect(globalCoordToRowSpecificCoord(sequence, 4)).toBe(3)
148
+ expect(globalColToSeqPos(sequence, 2)).toBe(2)
149
+ expect(globalColToSeqPos(sequence, 3)).toBe(2)
150
+ expect(globalColToSeqPos(sequence, 4)).toBe(3)
109
151
  // Position 5 is a gap (-), so count before it is 4
110
- expect(globalCoordToRowSpecificCoord(sequence, 5)).toBe(4)
111
- expect(globalCoordToRowSpecificCoord(sequence, 6)).toBe(4)
152
+ expect(globalColToSeqPos(sequence, 5)).toBe(4)
153
+ expect(globalColToSeqPos(sequence, 6)).toBe(4)
112
154
  })
113
155
 
114
- test('with no gaps in sequence', () => {
156
+ test('globalColToSeqPos with no gaps in sequence', () => {
115
157
  const sequence = 'ACGTA'
116
- expect(globalCoordToRowSpecificCoord(sequence, 0)).toBe(0)
117
- expect(globalCoordToRowSpecificCoord(sequence, 1)).toBe(1)
118
- expect(globalCoordToRowSpecificCoord(sequence, 2)).toBe(2)
119
- expect(globalCoordToRowSpecificCoord(sequence, 3)).toBe(3)
120
- expect(globalCoordToRowSpecificCoord(sequence, 4)).toBe(4)
158
+ expect(globalColToSeqPos(sequence, 0)).toBe(0)
159
+ expect(globalColToSeqPos(sequence, 1)).toBe(1)
160
+ expect(globalColToSeqPos(sequence, 2)).toBe(2)
161
+ expect(globalColToSeqPos(sequence, 3)).toBe(3)
162
+ expect(globalColToSeqPos(sequence, 4)).toBe(4)
121
163
  })
122
164
 
123
- test('with all gaps in sequence', () => {
165
+ test('globalColToSeqPos with all gaps in sequence', () => {
124
166
  const sequence = '-----'
125
- expect(globalCoordToRowSpecificCoord(sequence, 0)).toBe(0)
126
- expect(globalCoordToRowSpecificCoord(sequence, 1)).toBe(0)
127
- expect(globalCoordToRowSpecificCoord(sequence, 2)).toBe(0)
128
- expect(globalCoordToRowSpecificCoord(sequence, 3)).toBe(0)
129
- expect(globalCoordToRowSpecificCoord(sequence, 4)).toBe(0)
167
+ expect(globalColToSeqPos(sequence, 0)).toBe(0)
168
+ expect(globalColToSeqPos(sequence, 1)).toBe(0)
169
+ expect(globalColToSeqPos(sequence, 2)).toBe(0)
170
+ expect(globalColToSeqPos(sequence, 3)).toBe(0)
171
+ expect(globalColToSeqPos(sequence, 4)).toBe(0)
130
172
  })
131
173
 
132
- test('with position exceeding sequence length', () => {
174
+ test('globalColToSeqPos with position exceeding sequence length', () => {
133
175
  const sequence = 'AC-GT'
134
- expect(globalCoordToRowSpecificCoord(sequence, 10)).toBe(4)
176
+ expect(globalColToSeqPos(sequence, 10)).toBe(4)
135
177
  })
136
178
 
137
- test('mouseOverCoordToGapRemovedCoord', () => {
179
+ // Tests for visibleColToSeqPos (visible column → sequence position)
180
+ test('visibleColToSeqPos returns sequence position or undefined for gaps', () => {
138
181
  const seq = 'AC--GT--CT'
139
- expect(
140
- mouseOverCoordToGapRemovedCoord({ seq, position: 0, blanks: [] }),
141
- ).toBe(0)
142
- expect(
143
- mouseOverCoordToGapRemovedCoord({ seq, position: 1, blanks: [] }),
144
- ).toBe(1)
145
- expect(
146
- mouseOverCoordToGapRemovedCoord({ seq, position: 2, blanks: [] }),
147
- ).toBe(undefined)
148
- expect(
149
- mouseOverCoordToGapRemovedCoord({ seq, position: 3, blanks: [] }),
150
- ).toBe(undefined)
151
- expect(
152
- mouseOverCoordToGapRemovedCoord({ seq, position: 4, blanks: [] }),
153
- ).toBe(2)
154
- expect(
155
- mouseOverCoordToGapRemovedCoord({ seq, position: 5, blanks: [] }),
156
- ).toBe(3)
157
- expect(
158
- mouseOverCoordToGapRemovedCoord({ seq, position: 6, blanks: [] }),
159
- ).toBe(undefined)
160
- expect(
161
- mouseOverCoordToGapRemovedCoord({ seq, position: 7, blanks: [] }),
162
- ).toBe(undefined)
163
- expect(
164
- mouseOverCoordToGapRemovedCoord({ seq, position: 8, blanks: [] }),
165
- ).toBe(4)
166
- expect(
167
- mouseOverCoordToGapRemovedCoord({ seq, position: 9, blanks: [] }),
168
- ).toBe(5)
182
+ expect(visibleColToSeqPos({ seq, visibleCol: 0, blanks: [] })).toBe(0)
183
+ expect(visibleColToSeqPos({ seq, visibleCol: 1, blanks: [] })).toBe(1)
184
+ expect(visibleColToSeqPos({ seq, visibleCol: 2, blanks: [] })).toBe(undefined)
185
+ expect(visibleColToSeqPos({ seq, visibleCol: 3, blanks: [] })).toBe(undefined)
186
+ expect(visibleColToSeqPos({ seq, visibleCol: 4, blanks: [] })).toBe(2)
187
+ expect(visibleColToSeqPos({ seq, visibleCol: 5, blanks: [] })).toBe(3)
188
+ expect(visibleColToSeqPos({ seq, visibleCol: 6, blanks: [] })).toBe(undefined)
189
+ expect(visibleColToSeqPos({ seq, visibleCol: 7, blanks: [] })).toBe(undefined)
190
+ expect(visibleColToSeqPos({ seq, visibleCol: 8, blanks: [] })).toBe(4)
191
+ expect(visibleColToSeqPos({ seq, visibleCol: 9, blanks: [] })).toBe(5)
192
+ })
193
+
194
+ // Round-trip tests: visible → global → visible should be identity for valid columns
195
+ test('round-trip: visibleColToGlobalCol and globalColToVisibleCol are inverses', () => {
196
+ const blanks = [2, 5, 8]
197
+ // For each visible column, going to global and back should return the same value
198
+ for (let visibleCol = 0; visibleCol < 10; visibleCol++) {
199
+ const globalCol = visibleColToGlobalCol(blanks, visibleCol)
200
+ const backToVisible = globalColToVisibleCol(blanks, globalCol)
201
+ expect(backToVisible).toBe(visibleCol)
202
+ }
203
+ })
204
+
205
+ test('round-trip: globalColToVisibleCol and visibleColToGlobalCol for non-hidden columns', () => {
206
+ const blanks = [2, 5, 8]
207
+ // For each global column that is NOT hidden, going to visible and back should return the same value
208
+ for (let globalCol = 0; globalCol < 15; globalCol++) {
209
+ const visibleCol = globalColToVisibleCol(blanks, globalCol)
210
+ if (visibleCol !== undefined) {
211
+ const backToGlobal = visibleColToGlobalCol(blanks, visibleCol)
212
+ expect(backToGlobal).toBe(globalCol)
213
+ }
214
+ }
215
+ })
216
+
217
+ // Edge case: blanks at position 0
218
+ test('globalColToVisibleCol with blank at position 0', () => {
219
+ const blanks = [0, 1, 5]
220
+ expect(globalColToVisibleCol(blanks, 0)).toBe(undefined) // Hidden
221
+ expect(globalColToVisibleCol(blanks, 1)).toBe(undefined) // Hidden
222
+ expect(globalColToVisibleCol(blanks, 2)).toBe(0) // First visible column
223
+ expect(globalColToVisibleCol(blanks, 3)).toBe(1)
224
+ expect(globalColToVisibleCol(blanks, 4)).toBe(2)
225
+ expect(globalColToVisibleCol(blanks, 5)).toBe(undefined) // Hidden
226
+ expect(globalColToVisibleCol(blanks, 6)).toBe(3)
227
+ })
228
+
229
+ test('visibleColToGlobalCol with blank at position 0', () => {
230
+ const blanks = [0, 1, 5]
231
+ expect(visibleColToGlobalCol(blanks, 0)).toBe(2) // Skips 0, 1
232
+ expect(visibleColToGlobalCol(blanks, 1)).toBe(3)
233
+ expect(visibleColToGlobalCol(blanks, 2)).toBe(4)
234
+ expect(visibleColToGlobalCol(blanks, 3)).toBe(6) // Skips 5
235
+ })
236
+
237
+ // Edge case: all columns before a position are blanks
238
+ test('globalColToVisibleCol with many leading blanks', () => {
239
+ const blanks = [0, 1, 2, 3, 4]
240
+ expect(globalColToVisibleCol(blanks, 0)).toBe(undefined)
241
+ expect(globalColToVisibleCol(blanks, 4)).toBe(undefined)
242
+ expect(globalColToVisibleCol(blanks, 5)).toBe(0) // First visible
243
+ expect(globalColToVisibleCol(blanks, 6)).toBe(1)
244
+ expect(globalColToVisibleCol(blanks, 10)).toBe(5)
245
+ })
246
+
247
+ // Test visibleColToSeqPos with blanks (combined gap hiding and row gaps)
248
+ test('visibleColToSeqPos with both blanks and row gaps', () => {
249
+ // Sequence: A-C-G (global cols 0,1,2,3,4)
250
+ // If blanks = [1, 3], visible sequence becomes: A C G (visible cols 0,1,2)
251
+ const seq = 'A-C-G'
252
+ const blanks = [1, 3]
253
+
254
+ // Visible col 0 → global col 0 → 'A' → seqPos 0
255
+ expect(visibleColToSeqPos({ seq, visibleCol: 0, blanks })).toBe(0)
256
+
257
+ // Visible col 1 → global col 2 → 'C' → seqPos 1
258
+ expect(visibleColToSeqPos({ seq, visibleCol: 1, blanks })).toBe(1)
259
+
260
+ // Visible col 2 → global col 4 → 'G' → seqPos 2
261
+ expect(visibleColToSeqPos({ seq, visibleCol: 2, blanks })).toBe(2)
169
262
  })
@@ -1,101 +1,176 @@
1
1
  import { isBlank } from './util'
2
2
 
3
- export function mouseOverCoordToGlobalCoord(
4
- blanks: number[],
5
- position: number,
6
- ) {
7
- let mousePosition = 0 // Current position in mouse coordinates
8
- let blankArrayIndex = 0 // Current index in the blanks array
9
- let globalPosition = 0 // Position in global coordinates (return value)
3
+ /**
4
+ * MSA Coordinate Systems:
5
+ *
6
+ * 1. **Global Column (globalCol)**: The column index in the full, unfiltered MSA.
7
+ * Range: 0 to (MSA width - 1)
8
+ * This is the "true" column position before any gap-hiding is applied.
9
+ *
10
+ * 2. **Visible Column (visibleCol)**: The column index after hiding gappy columns.
11
+ * Range: 0 to (numColumns - 1) where numColumns = MSA width - blanks.length
12
+ * This is what the user sees on screen when "Hide columns w/ N% gaps" is enabled.
13
+ * When gap hiding is disabled, visibleCol === globalCol.
14
+ *
15
+ * 3. **Sequence Position (seqPos)**: The position within a specific row's ungapped sequence.
16
+ * Range: 0 to (ungapped sequence length - 1)
17
+ * This counts only non-gap characters ('-' and '.' are gaps).
18
+ * Each row can have different seqPos values for the same globalCol due to gaps.
19
+ */
20
+
21
+ /**
22
+ * Convert a visible column index to a global column index.
23
+ * This is used when translating mouse/screen coordinates to MSA coordinates.
24
+ *
25
+ * @param blanks - Sorted array of global column indices that are hidden
26
+ * @param visibleCol - The visible column index (what the user sees on screen)
27
+ * @returns The corresponding global column index in the full MSA
28
+ */
29
+ export function visibleColToGlobalCol(blanks: number[], visibleCol: number) {
30
+ let currentVisibleCol = 0
31
+ let blankArrayIndex = 0
32
+ let globalCol = 0
10
33
  const blanksLen = blanks.length
11
34
 
12
- // Iterate until we reach the target mouse position
13
- while (mousePosition < position) {
14
- // Skip any blank positions in the sequence
15
- // Check if the next position (globalPosition + 1) is blank by comparing
16
- // blanks[blankArrayIndex] - 1 with current globalPosition
35
+ // Skip any leading blank columns (blanks at the very beginning)
36
+ while (blankArrayIndex < blanksLen && blanks[blankArrayIndex] === globalCol) {
37
+ blankArrayIndex++
38
+ globalCol++
39
+ }
40
+
41
+ while (currentVisibleCol < visibleCol) {
42
+ currentVisibleCol++
43
+ globalCol++
44
+
45
+ // Skip any blank columns after incrementing
17
46
  while (
18
47
  blankArrayIndex < blanksLen &&
19
- blanks[blankArrayIndex]! - 1 === globalPosition
48
+ blanks[blankArrayIndex] === globalCol
20
49
  ) {
21
50
  blankArrayIndex++
22
- globalPosition++
51
+ globalCol++
23
52
  }
24
-
25
- // Move to next position
26
- mousePosition++
27
- globalPosition++
28
53
  }
29
54
 
30
- return globalPosition
55
+ return globalCol
31
56
  }
32
57
 
33
- export function globalCoordToRowSpecificCoord(seq: string, position: number) {
34
- // Initialize counter for non-gap characters
35
- let nonGapCount = 0
36
- // Initialize position counter
37
- let currentPosition = 0
38
- const sequenceLength = seq.length
39
-
40
- // Iterate until we reach the target position or end of sequence
41
- while (currentPosition < position && currentPosition < sequenceLength) {
42
- // If current character is not a gap, increment the non-gap counter
43
- if (!isBlank(seq[currentPosition])) {
44
- nonGapCount++
58
+ /**
59
+ * Convert a global column index to a visible column index.
60
+ * This is the inverse of visibleColToGlobalCol.
61
+ *
62
+ * @param blanks - Sorted array of global column indices that are hidden
63
+ * @param globalCol - The global column index in the full MSA
64
+ * @returns The visible column index, or undefined if the column is hidden
65
+ */
66
+ export function globalColToVisibleCol(
67
+ blanks: number[],
68
+ globalCol: number,
69
+ ): number | undefined {
70
+ // Check if this column is hidden
71
+ // Use binary search since blanks is sorted
72
+ let left = 0
73
+ let right = blanks.length - 1
74
+ while (left <= right) {
75
+ const mid = Math.floor((left + right) / 2)
76
+ if (blanks[mid] === globalCol) {
77
+ return undefined // Column is hidden
78
+ }
79
+ if (blanks[mid]! < globalCol) {
80
+ left = mid + 1
81
+ } else {
82
+ right = mid - 1
45
83
  }
46
- currentPosition++
47
84
  }
48
85
 
49
- return nonGapCount
86
+ // Count blanks before this column (left is now the insertion point)
87
+ const blanksBefore = left
88
+ return globalCol - blanksBefore
50
89
  }
51
90
 
52
- export function mouseOverCoordToGapRemovedRowCoord({
53
- rowName,
54
- position,
55
- rowMap,
56
- blanks,
57
- }: {
58
- rowName: string
59
- position: number
60
- rowMap: Map<string, string>
61
- blanks: number[]
62
- }) {
63
- const seq = rowMap.get(rowName)
64
- return seq !== undefined
65
- ? mouseOverCoordToGapRemovedCoord({
66
- seq,
67
- position,
68
- blanks,
69
- })
70
- : undefined
91
+ /**
92
+ * Convert a global column index to a row-specific sequence position.
93
+ * This counts non-gap characters up to the given global column.
94
+ *
95
+ * @param seq - The row's sequence string (including gaps)
96
+ * @param globalCol - The global column index
97
+ * @returns The sequence position (count of non-gap characters before this column)
98
+ */
99
+ export function globalColToSeqPos(seq: string, globalCol: number) {
100
+ let seqPos = 0
101
+ let currentCol = 0
102
+ const seqLen = seq.length
103
+
104
+ while (currentCol < globalCol && currentCol < seqLen) {
105
+ if (!isBlank(seq[currentCol])) {
106
+ seqPos++
107
+ }
108
+ currentCol++
109
+ }
110
+
111
+ return seqPos
71
112
  }
72
113
 
73
- export function mouseOverCoordToGapRemovedCoord({
114
+ /**
115
+ * Convert a visible column to a row-specific sequence position.
116
+ * Returns undefined if the position is a gap in the sequence.
117
+ *
118
+ * @param seq - The row's sequence string (including gaps)
119
+ * @param blanks - Sorted array of global column indices that are hidden
120
+ * @param visibleCol - The visible column index
121
+ * @returns The sequence position, or undefined if it's a gap
122
+ */
123
+ export function visibleColToSeqPos({
74
124
  seq,
75
125
  blanks,
76
- position,
126
+ visibleCol,
77
127
  }: {
78
128
  seq: string
79
129
  blanks: number[]
80
- position: number
130
+ visibleCol: number
81
131
  }) {
82
- // First convert the mouse position to global coordinates
83
- const globalPos = mouseOverCoordToGlobalCoord(blanks, position)
132
+ // First convert the visible column to global column
133
+ const globalCol = visibleColToGlobalCol(blanks, visibleCol)
84
134
  const seqLen = seq.length
85
135
 
86
136
  // Check if the position in the sequence is a gap
87
- if (globalPos < seqLen && isBlank(seq[globalPos])) {
137
+ if (globalCol < seqLen && isBlank(seq[globalCol])) {
88
138
  return undefined
89
139
  }
90
140
 
91
141
  // Count non-gap characters up to the global position
92
- let nonGapCount = 0
93
- for (let i = 0; i < globalPos && i < seqLen; i++) {
142
+ let seqPos = 0
143
+ for (let i = 0; i < globalCol && i < seqLen; i++) {
94
144
  if (!isBlank(seq[i])) {
95
- nonGapCount++
145
+ seqPos++
96
146
  }
97
147
  }
98
148
 
99
- // If we're at a valid position, return the count of non-gap characters
100
- return globalPos < seqLen ? nonGapCount : undefined
149
+ return globalCol < seqLen ? seqPos : undefined
150
+ }
151
+
152
+ /**
153
+ * Convert a visible column to a row-specific sequence position, with row lookup.
154
+ *
155
+ * @param rowName - The name of the row
156
+ * @param visibleCol - The visible column index
157
+ * @param rowMap - Map from row name to sequence string
158
+ * @param blanks - Sorted array of global column indices that are hidden
159
+ * @returns The sequence position, or undefined if row not found or position is a gap
160
+ */
161
+ export function visibleColToSeqPosForRow({
162
+ rowName,
163
+ visibleCol,
164
+ rowMap,
165
+ blanks,
166
+ }: {
167
+ rowName: string
168
+ visibleCol: number
169
+ rowMap: Map<string, string>
170
+ blanks: number[]
171
+ }) {
172
+ const seq = rowMap.get(rowName)
173
+ return seq !== undefined
174
+ ? visibleColToSeqPos({ seq, visibleCol, blanks })
175
+ : undefined
101
176
  }
@@ -0,0 +1,71 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { seqPosToGlobalCol } from './seqPosToGlobalCol'
4
+
5
+ describe('seqPosToGlobalCol', () => {
6
+ test('converts sequence position to global column with no gaps', () => {
7
+ const row = 'ATGCATGC'
8
+ expect(seqPosToGlobalCol({ row, seqPos: 0 })).toBe(0)
9
+ expect(seqPosToGlobalCol({ row, seqPos: 3 })).toBe(3)
10
+ expect(seqPosToGlobalCol({ row, seqPos: 7 })).toBe(7)
11
+ expect(seqPosToGlobalCol({ row, seqPos: 8 })).toBe(8) // Past end
12
+ })
13
+
14
+ test('converts sequence position to global column with gaps', () => {
15
+ const row = 'A-TG-CA-TGC'
16
+ // Global: A(0) -(1) T(2) G(3) -(4) C(5) A(6) -(7) T(8) G(9) C(10)
17
+ // SeqPos: A(0) T(1) G(2) C(3) A(4) T(5) G(6) C(7)
18
+
19
+ expect(seqPosToGlobalCol({ row, seqPos: 0 })).toBe(0) // A
20
+ expect(seqPosToGlobalCol({ row, seqPos: 1 })).toBe(2) // T
21
+ expect(seqPosToGlobalCol({ row, seqPos: 2 })).toBe(3) // G
22
+ expect(seqPosToGlobalCol({ row, seqPos: 3 })).toBe(5) // C
23
+ expect(seqPosToGlobalCol({ row, seqPos: 4 })).toBe(6) // A
24
+ expect(seqPosToGlobalCol({ row, seqPos: 5 })).toBe(8) // T
25
+ expect(seqPosToGlobalCol({ row, seqPos: 6 })).toBe(9) // G
26
+ expect(seqPosToGlobalCol({ row, seqPos: 7 })).toBe(10) // C
27
+ expect(seqPosToGlobalCol({ row, seqPos: 8 })).toBe(11) // Past end
28
+ })
29
+
30
+ test('handles empty row', () => {
31
+ expect(seqPosToGlobalCol({ row: '', seqPos: 0 })).toBe(0)
32
+ })
33
+
34
+ test('handles row with only gaps', () => {
35
+ const row = '---..--'
36
+ expect(seqPosToGlobalCol({ row, seqPos: 0 })).toBe(0)
37
+ expect(seqPosToGlobalCol({ row, seqPos: 1 })).toBe(7) // Past end
38
+ })
39
+
40
+ test('handles mixed gap characters (- and .)', () => {
41
+ const row = 'A-.G-C.'
42
+ // Global: A(0) -(1) .(2) G(3) -(4) C(5) .(6)
43
+ // SeqPos: A(0) G(1) C(2)
44
+
45
+ expect(seqPosToGlobalCol({ row, seqPos: 0 })).toBe(0) // A
46
+ expect(seqPosToGlobalCol({ row, seqPos: 1 })).toBe(3) // G
47
+ expect(seqPosToGlobalCol({ row, seqPos: 2 })).toBe(5) // C
48
+ expect(seqPosToGlobalCol({ row, seqPos: 3 })).toBe(7) // Past end
49
+ })
50
+
51
+ test('handles leading gaps', () => {
52
+ const row = '--ACG'
53
+ // Global: -(0) -(1) A(2) C(3) G(4)
54
+ // SeqPos: A(0) C(1) G(2)
55
+
56
+ expect(seqPosToGlobalCol({ row, seqPos: 0 })).toBe(2) // A
57
+ expect(seqPosToGlobalCol({ row, seqPos: 1 })).toBe(3) // C
58
+ expect(seqPosToGlobalCol({ row, seqPos: 2 })).toBe(4) // G
59
+ })
60
+
61
+ test('handles trailing gaps', () => {
62
+ const row = 'ACG--'
63
+ // Global: A(0) C(1) G(2) -(3) -(4)
64
+ // SeqPos: A(0) C(1) G(2)
65
+
66
+ expect(seqPosToGlobalCol({ row, seqPos: 0 })).toBe(0) // A
67
+ expect(seqPosToGlobalCol({ row, seqPos: 1 })).toBe(1) // C
68
+ expect(seqPosToGlobalCol({ row, seqPos: 2 })).toBe(2) // G
69
+ expect(seqPosToGlobalCol({ row, seqPos: 3 })).toBe(5) // Past end
70
+ })
71
+ })
@@ -0,0 +1,40 @@
1
+ import { isBlank } from './util'
2
+
3
+ /**
4
+ * Convert a sequence position (ungapped, 0-based) to a global column index.
5
+ * This finds the global column that contains the Nth non-gap character.
6
+ *
7
+ * @param row - The row's sequence string (including gaps)
8
+ * @param seqPos - The sequence position (0-based count of non-gap characters)
9
+ * @returns The global column index containing the seqPos-th non-gap character
10
+ *
11
+ * @example
12
+ * // Row: "A-TG-C" (A at 0, T at 2, G at 3, C at 5)
13
+ * seqPosToGlobalCol({ row: "A-TG-C", seqPos: 0 }) // → 0 (A)
14
+ * seqPosToGlobalCol({ row: "A-TG-C", seqPos: 1 }) // → 2 (T)
15
+ * seqPosToGlobalCol({ row: "A-TG-C", seqPos: 2 }) // → 3 (G)
16
+ * seqPosToGlobalCol({ row: "A-TG-C", seqPos: 3 }) // → 5 (C)
17
+ */
18
+ export function seqPosToGlobalCol({
19
+ row,
20
+ seqPos,
21
+ }: {
22
+ row: string
23
+ seqPos: number
24
+ }) {
25
+ let nonGapCount = 0
26
+ let globalCol = 0
27
+ // Find the seqPos-th non-gap character
28
+ while (globalCol < row.length) {
29
+ if (!isBlank(row[globalCol])) {
30
+ if (nonGapCount === seqPos) {
31
+ return globalCol
32
+ }
33
+ nonGapCount++
34
+ }
35
+ globalCol++
36
+ }
37
+ // If seqPos is 0 and we didn't find any non-gap character, return 0
38
+ // Otherwise return globalCol (which is row.length at this point)
39
+ return seqPos === 0 ? 0 : globalCol
40
+ }