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.
- package/bundle/index.js +99 -99
- package/bundle/index.js.LICENSE.txt +6 -6
- package/bundle/index.js.map +1 -1
- package/dist/__snapshots__/parseAsn1.test.js.snap +2400 -0
- package/dist/components/header/HeaderInfoArea.js +3 -4
- package/dist/components/header/HeaderInfoArea.js.map +1 -1
- package/dist/components/import/ImportForm.js +6 -2
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/import/util.d.ts +1 -1
- package/dist/components/import/util.js +4 -1
- package/dist/components/import/util.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +7 -2
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +10 -8
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/model.d.ts +153 -16
- package/dist/model.js +97 -29
- package/dist/model.js.map +1 -1
- package/dist/rowCoordinateCalculations.d.ts +69 -9
- package/dist/rowCoordinateCalculations.js +118 -46
- package/dist/rowCoordinateCalculations.js.map +1 -1
- package/dist/rowCoordinateCalculations.test.js +152 -52
- package/dist/rowCoordinateCalculations.test.js.map +1 -1
- package/dist/seqPosToGlobalCol.d.ts +19 -0
- package/dist/seqPosToGlobalCol.js +34 -0
- package/dist/seqPosToGlobalCol.js.map +1 -0
- package/dist/seqPosToGlobalCol.test.js +60 -0
- package/dist/seqPosToGlobalCol.test.js.map +1 -0
- package/dist/util.d.ts +1 -2
- package/dist/util.js +0 -9
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -9
- package/src/components/header/HeaderInfoArea.tsx +2 -5
- package/src/components/import/ImportForm.tsx +6 -1
- package/src/components/import/util.ts +4 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +7 -2
- package/src/components/msa/renderMSABlock.ts +5 -1
- package/src/components/tree/renderTreeCanvas.ts +11 -9
- package/src/declare.d.ts +0 -1
- package/src/model.ts +122 -42
- package/src/rowCoordinateCalculations.test.ts +167 -74
- package/src/rowCoordinateCalculations.ts +138 -63
- package/src/seqPosToGlobalCol.test.ts +71 -0
- package/src/seqPosToGlobalCol.ts +40 -0
- package/src/util.ts +1 -19
- package/src/version.ts +1 -1
- package/dist/parseGFF.d.ts +0 -10
- package/dist/parseGFF.js +0 -31
- package/dist/parseGFF.js.map +0 -1
- package/dist/parseNewick.d.ts +0 -60
- package/dist/parseNewick.js +0 -95
- package/dist/parseNewick.js.map +0 -1
- package/dist/parsers/A3mMSA.d.ts +0 -43
- package/dist/parsers/A3mMSA.js +0 -277
- package/dist/parsers/A3mMSA.js.map +0 -1
- package/dist/parsers/A3mMSA.test.js +0 -138
- package/dist/parsers/A3mMSA.test.js.map +0 -1
- package/dist/parsers/ClustalMSA.d.ts +0 -30
- package/dist/parsers/ClustalMSA.js +0 -55
- package/dist/parsers/ClustalMSA.js.map +0 -1
- package/dist/parsers/EmfMSA.d.ts +0 -27
- package/dist/parsers/EmfMSA.js +0 -53
- package/dist/parsers/EmfMSA.js.map +0 -1
- package/dist/parsers/EmfTree.d.ts +0 -5
- package/dist/parsers/EmfTree.js +0 -8
- package/dist/parsers/EmfTree.js.map +0 -1
- package/dist/parsers/FastaMSA.d.ts +0 -19
- package/dist/parsers/FastaMSA.js +0 -69
- package/dist/parsers/FastaMSA.js.map +0 -1
- package/dist/parsers/StockholmMSA.d.ts +0 -68
- package/dist/parsers/StockholmMSA.js +0 -107
- package/dist/parsers/StockholmMSA.js.map +0 -1
- package/dist/seqCoordToRowSpecificGlobalCoord.d.ts +0 -4
- package/dist/seqCoordToRowSpecificGlobalCoord.js +0 -19
- package/dist/seqCoordToRowSpecificGlobalCoord.js.map +0 -1
- package/dist/seqCoordToRowSpecificGlobalCoord.test.d.ts +0 -1
- package/dist/seqCoordToRowSpecificGlobalCoord.test.js +0 -42
- package/dist/seqCoordToRowSpecificGlobalCoord.test.js.map +0 -1
- package/src/parseGFF.ts +0 -34
- package/src/parseNewick.ts +0 -94
- package/src/parsers/A3mMSA.test.ts +0 -164
- package/src/parsers/A3mMSA.ts +0 -321
- package/src/parsers/ClustalMSA.ts +0 -69
- package/src/parsers/EmfMSA.ts +0 -67
- package/src/parsers/EmfTree.ts +0 -9
- package/src/parsers/FastaMSA.ts +0 -82
- package/src/parsers/StockholmMSA.ts +0 -140
- package/src/seqCoordToRowSpecificGlobalCoord.test.ts +0 -53
- package/src/seqCoordToRowSpecificGlobalCoord.ts +0 -25
- /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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
globalColToSeqPos,
|
|
5
|
+
globalColToVisibleCol,
|
|
6
|
+
visibleColToGlobalCol,
|
|
7
|
+
visibleColToSeqPos,
|
|
7
8
|
} from './rowCoordinateCalculations'
|
|
8
9
|
|
|
9
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
86
|
+
expect(visibleColToGlobalCol(blanks, r[0])).toBe(r[1])
|
|
85
87
|
})
|
|
86
88
|
})
|
|
87
89
|
|
|
88
|
-
|
|
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(
|
|
91
|
-
expect(
|
|
92
|
-
expect(
|
|
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(
|
|
95
|
-
expect(
|
|
96
|
-
expect(
|
|
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(
|
|
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(
|
|
104
|
-
expect(
|
|
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(
|
|
107
|
-
expect(
|
|
108
|
-
expect(
|
|
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(
|
|
111
|
-
expect(
|
|
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(
|
|
117
|
-
expect(
|
|
118
|
-
expect(
|
|
119
|
-
expect(
|
|
120
|
-
expect(
|
|
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(
|
|
126
|
-
expect(
|
|
127
|
-
expect(
|
|
128
|
-
expect(
|
|
129
|
-
expect(
|
|
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(
|
|
176
|
+
expect(globalColToSeqPos(sequence, 10)).toBe(4)
|
|
135
177
|
})
|
|
136
178
|
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
).toBe(
|
|
142
|
-
expect(
|
|
143
|
-
|
|
144
|
-
).toBe(
|
|
145
|
-
expect(
|
|
146
|
-
|
|
147
|
-
).toBe(
|
|
148
|
-
expect(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
//
|
|
13
|
-
while (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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]
|
|
48
|
+
blanks[blankArrayIndex] === globalCol
|
|
20
49
|
) {
|
|
21
50
|
blankArrayIndex++
|
|
22
|
-
|
|
51
|
+
globalCol++
|
|
23
52
|
}
|
|
24
|
-
|
|
25
|
-
// Move to next position
|
|
26
|
-
mousePosition++
|
|
27
|
-
globalPosition++
|
|
28
53
|
}
|
|
29
54
|
|
|
30
|
-
return
|
|
55
|
+
return globalCol
|
|
31
56
|
}
|
|
32
57
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
+
visibleCol,
|
|
77
127
|
}: {
|
|
78
128
|
seq: string
|
|
79
129
|
blanks: number[]
|
|
80
|
-
|
|
130
|
+
visibleCol: number
|
|
81
131
|
}) {
|
|
82
|
-
// First convert the
|
|
83
|
-
const
|
|
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 (
|
|
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
|
|
93
|
-
for (let i = 0; i <
|
|
142
|
+
let seqPos = 0
|
|
143
|
+
for (let i = 0; i < globalCol && i < seqLen; i++) {
|
|
94
144
|
if (!isBlank(seq[i])) {
|
|
95
|
-
|
|
145
|
+
seqPos++
|
|
96
146
|
}
|
|
97
147
|
}
|
|
98
148
|
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
}
|