react-msaview 4.4.0 → 4.4.2
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 +15 -15
- package/bundle/index.js.LICENSE.txt +5 -13
- package/bundle/index.js.map +1 -1
- package/dist/colorSchemes.js +2 -2
- package/dist/colorSchemes.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +2 -2
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/SettingsDialog.js +3 -2
- package/dist/components/dialogs/SettingsDialog.js.map +1 -1
- package/dist/components/header/Header.js +1 -1
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HeaderInfoArea.js +3 -2
- package/dist/components/header/HeaderInfoArea.js.map +1 -1
- package/dist/components/header/HeaderMenu.js +15 -97
- package/dist/components/header/HeaderMenu.js.map +1 -1
- package/dist/components/header/SettingsMenu.js +57 -73
- package/dist/components/header/SettingsMenu.js.map +1 -1
- package/dist/components/header/ZoomMenu.js +14 -2
- package/dist/components/header/ZoomMenu.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +4 -1
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +4 -4
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +13 -9
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/model.d.ts +17 -5
- package/dist/model.js +70 -57
- package/dist/model.js.map +1 -1
- package/dist/rowCoordinateCalculations.d.ts +13 -2
- package/dist/rowCoordinateCalculations.js +60 -17
- package/dist/rowCoordinateCalculations.js.map +1 -1
- package/dist/rowCoordinateCalculations.test.js +96 -2
- package/dist/rowCoordinateCalculations.test.js.map +1 -1
- package/dist/seqCoordToRowSpecificGlobalCoord.d.ts +4 -0
- package/dist/seqCoordToRowSpecificGlobalCoord.js +15 -0
- package/dist/seqCoordToRowSpecificGlobalCoord.js.map +1 -0
- package/dist/seqCoordToRowSpecificGlobalCoord.test.d.ts +1 -0
- package/dist/seqCoordToRowSpecificGlobalCoord.test.js +42 -0
- package/dist/seqCoordToRowSpecificGlobalCoord.test.js.map +1 -0
- package/dist/util.d.ts +1 -6
- package/dist/util.js +5 -22
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/colorSchemes.ts +2 -2
- package/src/components/VerticalScrollbar.tsx +2 -3
- package/src/components/dialogs/SettingsDialog.tsx +4 -2
- package/src/components/header/Header.tsx +1 -1
- package/src/components/header/HeaderInfoArea.tsx +5 -2
- package/src/components/header/HeaderMenu.tsx +15 -110
- package/src/components/header/SettingsMenu.tsx +64 -81
- package/src/components/header/ZoomMenu.tsx +15 -2
- package/src/components/msa/MSAMouseoverCanvas.tsx +4 -1
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +4 -4
- package/src/components/msa/renderMSABlock.ts +26 -22
- package/src/model.ts +89 -67
- package/src/rowCoordinateCalculations.test.ts +138 -2
- package/src/rowCoordinateCalculations.ts +95 -18
- package/src/seqCoordToRowSpecificGlobalCoord.test.ts +53 -0
- package/src/seqCoordToRowSpecificGlobalCoord.ts +20 -0
- package/src/util.ts +5 -28
- package/src/version.ts +1 -1
package/src/model.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
clamp,
|
|
3
|
+
fetchAndMaybeUnzipText,
|
|
4
|
+
groupBy,
|
|
5
|
+
localStorageGetBoolean,
|
|
6
|
+
localStorageSetBoolean,
|
|
7
|
+
notEmpty,
|
|
8
|
+
sum,
|
|
9
|
+
} from '@jbrowse/core/util'
|
|
2
10
|
import { openLocation } from '@jbrowse/core/util/io'
|
|
3
11
|
import { ElementId, FileLocation } from '@jbrowse/core/util/types/mst'
|
|
4
12
|
import { colord } from 'colord'
|
|
@@ -8,7 +16,6 @@ import { parseEmfTree } from 'emf-js'
|
|
|
8
16
|
import { saveAs } from 'file-saver'
|
|
9
17
|
import { autorun, transaction } from 'mobx'
|
|
10
18
|
import { addDisposer, cast, types } from 'mobx-state-tree'
|
|
11
|
-
import { ungzip } from 'pako'
|
|
12
19
|
import Stockholm from 'stockholm-js'
|
|
13
20
|
|
|
14
21
|
import { blocksX, blocksY } from './calculateBlocks'
|
|
@@ -29,17 +36,15 @@ import FastaMSA from './parsers/FastaMSA'
|
|
|
29
36
|
import StockholmMSA from './parsers/StockholmMSA'
|
|
30
37
|
import { reparseTree } from './reparseTree'
|
|
31
38
|
import {
|
|
32
|
-
|
|
39
|
+
mouseOverCoordToGapRemovedRowCoord,
|
|
33
40
|
mouseOverCoordToGlobalCoord,
|
|
34
41
|
} from './rowCoordinateCalculations'
|
|
42
|
+
import { seqCoordToRowSpecificGlobalCoord } from './seqCoordToRowSpecificGlobalCoord'
|
|
35
43
|
import {
|
|
36
|
-
clamp,
|
|
37
44
|
collapse,
|
|
38
45
|
generateNodeIds,
|
|
39
|
-
|
|
46
|
+
isBlank,
|
|
40
47
|
len,
|
|
41
|
-
localStorageGetBoolean,
|
|
42
|
-
localStorageSetBoolean,
|
|
43
48
|
maxLength,
|
|
44
49
|
setBrLength,
|
|
45
50
|
skipBlanks,
|
|
@@ -627,24 +632,23 @@ function stateModelFactory() {
|
|
|
627
632
|
*/
|
|
628
633
|
get tree(): NodeWithIds {
|
|
629
634
|
const text = self.data.tree
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
635
|
+
|
|
636
|
+
return reparseTree(
|
|
637
|
+
text
|
|
638
|
+
? generateNodeIds(
|
|
633
639
|
text.startsWith('BioTreeContainer')
|
|
634
640
|
? flatToTree(parseAsn1(text))
|
|
635
641
|
: parseNewick(
|
|
636
642
|
text.startsWith('SEQ') ? parseEmfTree(text).tree : text,
|
|
637
643
|
),
|
|
638
|
-
)
|
|
639
|
-
)
|
|
640
|
-
: reparseTree(
|
|
641
|
-
this.MSA?.getTree() || {
|
|
644
|
+
)
|
|
645
|
+
: this.MSA?.getTree() || {
|
|
642
646
|
noTree: true,
|
|
643
647
|
children: [],
|
|
644
648
|
id: 'empty',
|
|
645
649
|
name: 'empty',
|
|
646
650
|
},
|
|
647
|
-
|
|
651
|
+
)
|
|
648
652
|
},
|
|
649
653
|
|
|
650
654
|
/**
|
|
@@ -711,16 +715,18 @@ function stateModelFactory() {
|
|
|
711
715
|
if (hideGaps) {
|
|
712
716
|
const strs = this.leaves
|
|
713
717
|
.map(leaf => this.MSA?.getRow(leaf.data.name))
|
|
714
|
-
.filter(
|
|
718
|
+
.filter(notEmpty)
|
|
715
719
|
if (strs.length) {
|
|
716
|
-
|
|
720
|
+
const s0len = strs[0]!.length
|
|
721
|
+
for (let i = 0; i < s0len; i++) {
|
|
717
722
|
let counter = 0
|
|
718
|
-
|
|
719
|
-
|
|
723
|
+
const l = strs.length
|
|
724
|
+
for (let j = 0; j < l; j++) {
|
|
725
|
+
if (isBlank(strs[j]![i])) {
|
|
720
726
|
counter++
|
|
721
727
|
}
|
|
722
728
|
}
|
|
723
|
-
if (counter /
|
|
729
|
+
if (counter / l >= realAllowedGappyness / 100) {
|
|
724
730
|
blanks.push(i)
|
|
725
731
|
}
|
|
726
732
|
}
|
|
@@ -805,11 +811,7 @@ function stateModelFactory() {
|
|
|
805
811
|
* #getter
|
|
806
812
|
*/
|
|
807
813
|
get colStatsSums() {
|
|
808
|
-
return Object.
|
|
809
|
-
Object.entries(this.colStats).map(([key, val]) => {
|
|
810
|
-
return [key, sum(Object.values(val))]
|
|
811
|
-
}),
|
|
812
|
-
)
|
|
814
|
+
return this.colStats.map(val => sum(Object.values(val)))
|
|
813
815
|
},
|
|
814
816
|
/**
|
|
815
817
|
* #getter
|
|
@@ -948,14 +950,14 @@ function stateModelFactory() {
|
|
|
948
950
|
*/
|
|
949
951
|
zoomOutHorizontal() {
|
|
950
952
|
self.colWidth = Math.max(1, Math.floor(self.colWidth * 0.75))
|
|
951
|
-
self.scrollX = clamp(self.
|
|
953
|
+
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
952
954
|
},
|
|
953
955
|
/**
|
|
954
956
|
* #action
|
|
955
957
|
*/
|
|
956
958
|
zoomInHorizontal() {
|
|
957
959
|
self.colWidth = Math.ceil(self.colWidth * 1.5)
|
|
958
|
-
self.scrollX = clamp(self.
|
|
960
|
+
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
959
961
|
},
|
|
960
962
|
/**
|
|
961
963
|
* #action
|
|
@@ -976,7 +978,7 @@ function stateModelFactory() {
|
|
|
976
978
|
transaction(() => {
|
|
977
979
|
self.colWidth = Math.ceil(self.colWidth * 1.5)
|
|
978
980
|
self.rowHeight = Math.ceil(self.rowHeight * 1.5)
|
|
979
|
-
self.scrollX = clamp(self.
|
|
981
|
+
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
980
982
|
})
|
|
981
983
|
},
|
|
982
984
|
/**
|
|
@@ -986,7 +988,7 @@ function stateModelFactory() {
|
|
|
986
988
|
transaction(() => {
|
|
987
989
|
self.colWidth = Math.max(1, Math.floor(self.colWidth * 0.75))
|
|
988
990
|
self.rowHeight = Math.max(1.5, Math.floor(self.rowHeight * 0.75))
|
|
989
|
-
self.scrollX = clamp(self.
|
|
991
|
+
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
990
992
|
})
|
|
991
993
|
},
|
|
992
994
|
/**
|
|
@@ -1000,21 +1002,21 @@ function stateModelFactory() {
|
|
|
1000
1002
|
* #action
|
|
1001
1003
|
*/
|
|
1002
1004
|
doScrollY(deltaY: number) {
|
|
1003
|
-
self.scrollY = clamp(
|
|
1005
|
+
self.scrollY = clamp(self.scrollY + deltaY, -self.totalHeight + 10, 0)
|
|
1004
1006
|
},
|
|
1005
1007
|
|
|
1006
1008
|
/**
|
|
1007
1009
|
* #action
|
|
1008
1010
|
*/
|
|
1009
1011
|
doScrollX(deltaX: number) {
|
|
1010
|
-
self.scrollX = clamp(self.
|
|
1012
|
+
self.scrollX = clamp(self.scrollX + deltaX, self.maxScrollX, 0)
|
|
1011
1013
|
},
|
|
1012
1014
|
|
|
1013
1015
|
/**
|
|
1014
1016
|
* #action
|
|
1015
1017
|
*/
|
|
1016
1018
|
setScrollX(n: number) {
|
|
1017
|
-
self.scrollX = clamp(self.maxScrollX,
|
|
1019
|
+
self.scrollX = clamp(n, self.maxScrollX, 0)
|
|
1018
1020
|
},
|
|
1019
1021
|
|
|
1020
1022
|
/**
|
|
@@ -1137,11 +1139,9 @@ function stateModelFactory() {
|
|
|
1137
1139
|
* #method
|
|
1138
1140
|
* return a row-specific letter, or undefined if gap
|
|
1139
1141
|
*/
|
|
1140
|
-
mouseOverCoordToRowLetter(rowName: string,
|
|
1142
|
+
mouseOverCoordToRowLetter(rowName: string, pos: number) {
|
|
1141
1143
|
const { rowMap, blanks } = self
|
|
1142
|
-
return rowMap.get(rowName)?.[
|
|
1143
|
-
mouseOverCoordToGlobalCoord(blanks, position)
|
|
1144
|
-
]
|
|
1144
|
+
return rowMap.get(rowName)?.[mouseOverCoordToGlobalCoord(blanks, pos)]
|
|
1145
1145
|
},
|
|
1146
1146
|
|
|
1147
1147
|
/**
|
|
@@ -1150,15 +1150,25 @@ function stateModelFactory() {
|
|
|
1150
1150
|
* global coordinate
|
|
1151
1151
|
*/
|
|
1152
1152
|
mouseOverCoordToGapRemovedRowCoord(rowName: string, position: number) {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1153
|
+
return mouseOverCoordToGapRemovedRowCoord({
|
|
1154
|
+
rowName,
|
|
1155
|
+
position,
|
|
1156
|
+
rowMap: self.rowMap,
|
|
1157
|
+
blanks: self.blanks,
|
|
1158
|
+
})
|
|
1159
|
+
},
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* #method
|
|
1163
|
+
* return a row-specific sequence coordinate, skipping gaps, given a
|
|
1164
|
+
* global coordinate
|
|
1165
|
+
*/
|
|
1166
|
+
mouseOverCoordToGapRemovedRowCoordOneBased(
|
|
1167
|
+
rowName: string,
|
|
1168
|
+
position: number,
|
|
1169
|
+
) {
|
|
1170
|
+
const val = this.mouseOverCoordToGapRemovedRowCoord(rowName, position)
|
|
1171
|
+
return val !== undefined ? val + 1 : undefined
|
|
1162
1172
|
},
|
|
1163
1173
|
|
|
1164
1174
|
/**
|
|
@@ -1169,21 +1179,12 @@ function stateModelFactory() {
|
|
|
1169
1179
|
seqCoordToRowSpecificGlobalCoord(rowName: string, position: number) {
|
|
1170
1180
|
const { rowNames, rows } = self
|
|
1171
1181
|
const index = rowNames.indexOf(rowName)
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
if (row[i] !== '-') {
|
|
1179
|
-
k++
|
|
1180
|
-
} else if (k >= position) {
|
|
1181
|
-
break
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
return i
|
|
1185
|
-
}
|
|
1186
|
-
return 0
|
|
1182
|
+
return index !== -1 && rows[index]
|
|
1183
|
+
? seqCoordToRowSpecificGlobalCoord({
|
|
1184
|
+
row: rows[index][1],
|
|
1185
|
+
position,
|
|
1186
|
+
})
|
|
1187
|
+
: 0
|
|
1187
1188
|
},
|
|
1188
1189
|
}))
|
|
1189
1190
|
|
|
@@ -1321,7 +1322,11 @@ function stateModelFactory() {
|
|
|
1321
1322
|
* #action
|
|
1322
1323
|
*/
|
|
1323
1324
|
reset() {
|
|
1324
|
-
self.setData({
|
|
1325
|
+
self.setData({
|
|
1326
|
+
tree: '',
|
|
1327
|
+
msa: '',
|
|
1328
|
+
})
|
|
1329
|
+
self.resetZoom()
|
|
1325
1330
|
self.setError(undefined)
|
|
1326
1331
|
self.setScrollY(0)
|
|
1327
1332
|
self.setScrollX(0)
|
|
@@ -1368,12 +1373,26 @@ function stateModelFactory() {
|
|
|
1368
1373
|
/**
|
|
1369
1374
|
* #action
|
|
1370
1375
|
*/
|
|
1371
|
-
|
|
1376
|
+
fit() {
|
|
1372
1377
|
self.rowHeight = self.msaAreaHeight / self.numRows
|
|
1373
1378
|
self.colWidth = self.msaAreaWidth / self.numColumns
|
|
1374
1379
|
self.scrollX = 0
|
|
1375
1380
|
self.scrollY = 0
|
|
1376
1381
|
},
|
|
1382
|
+
/**
|
|
1383
|
+
* #action
|
|
1384
|
+
*/
|
|
1385
|
+
fitVertically() {
|
|
1386
|
+
self.rowHeight = self.msaAreaHeight / self.numRows
|
|
1387
|
+
self.scrollY = 0
|
|
1388
|
+
},
|
|
1389
|
+
/**
|
|
1390
|
+
* #action
|
|
1391
|
+
*/
|
|
1392
|
+
fitHorizontally() {
|
|
1393
|
+
self.colWidth = self.msaAreaWidth / self.numColumns
|
|
1394
|
+
self.scrollX = 0
|
|
1395
|
+
},
|
|
1377
1396
|
|
|
1378
1397
|
afterCreate() {
|
|
1379
1398
|
addDisposer(
|
|
@@ -1402,7 +1421,7 @@ function stateModelFactory() {
|
|
|
1402
1421
|
try {
|
|
1403
1422
|
self.setLoadingTree(true)
|
|
1404
1423
|
self.setTree(
|
|
1405
|
-
await openLocation(treeFilehandle)
|
|
1424
|
+
await fetchAndMaybeUnzipText(openLocation(treeFilehandle)),
|
|
1406
1425
|
)
|
|
1407
1426
|
if (treeFilehandle.locationType === 'BlobLocation') {
|
|
1408
1427
|
// clear filehandle after loading if from a local file
|
|
@@ -1425,7 +1444,9 @@ function stateModelFactory() {
|
|
|
1425
1444
|
if (treeMetadataFilehandle) {
|
|
1426
1445
|
try {
|
|
1427
1446
|
self.setTreeMetadata(
|
|
1428
|
-
await
|
|
1447
|
+
await fetchAndMaybeUnzipText(
|
|
1448
|
+
openLocation(treeMetadataFilehandle),
|
|
1449
|
+
),
|
|
1429
1450
|
)
|
|
1430
1451
|
} catch (e) {
|
|
1431
1452
|
console.error(e)
|
|
@@ -1443,9 +1464,10 @@ function stateModelFactory() {
|
|
|
1443
1464
|
if (msaFilehandle) {
|
|
1444
1465
|
try {
|
|
1445
1466
|
self.setLoadingMSA(true)
|
|
1446
|
-
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1467
|
+
self.setError(undefined)
|
|
1468
|
+
const txt = await fetchAndMaybeUnzipText(
|
|
1469
|
+
openLocation(msaFilehandle),
|
|
1470
|
+
)
|
|
1449
1471
|
transaction(() => {
|
|
1450
1472
|
self.setMSA(txt)
|
|
1451
1473
|
if (msaFilehandle.locationType === 'BlobLocation') {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { expect, test } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
globalCoordToRowSpecificCoord,
|
|
5
|
+
mouseOverCoordToGapRemovedCoord,
|
|
6
|
+
mouseOverCoordToGlobalCoord,
|
|
7
|
+
} from './rowCoordinateCalculations'
|
|
8
|
+
|
|
9
|
+
test('with blanks at positions [2, 5, 8]', () => {
|
|
5
10
|
const blanks = [2, 5, 8]
|
|
6
11
|
;(
|
|
7
12
|
[
|
|
@@ -18,3 +23,134 @@ test('blanks3', () => {
|
|
|
18
23
|
expect(mouseOverCoordToGlobalCoord(blanks, r[0])).toBe(r[1])
|
|
19
24
|
})
|
|
20
25
|
})
|
|
26
|
+
|
|
27
|
+
test('with no blanks', () => {
|
|
28
|
+
const blanks: number[] = []
|
|
29
|
+
;(
|
|
30
|
+
[
|
|
31
|
+
[0, 0],
|
|
32
|
+
[1, 1],
|
|
33
|
+
[5, 5],
|
|
34
|
+
[10, 10],
|
|
35
|
+
] as const
|
|
36
|
+
).forEach(r => {
|
|
37
|
+
expect(mouseOverCoordToGlobalCoord(blanks, r[0])).toBe(r[1])
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('with consecutive blanks', () => {
|
|
42
|
+
const blanks = [2, 3, 4, 7, 8]
|
|
43
|
+
;(
|
|
44
|
+
[
|
|
45
|
+
[0, 0],
|
|
46
|
+
[1, 1],
|
|
47
|
+
[2, 5], // After position 1, skip 3 blanks (2,3,4)
|
|
48
|
+
[3, 6], // Next position
|
|
49
|
+
[4, 9], // After position 3, skip 2 blanks (7,8)
|
|
50
|
+
[5, 10],
|
|
51
|
+
] as const
|
|
52
|
+
).forEach(r => {
|
|
53
|
+
expect(mouseOverCoordToGlobalCoord(blanks, r[0])).toBe(r[1])
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('with blanks at the beginning', () => {
|
|
58
|
+
const blanks = [1, 2, 5]
|
|
59
|
+
;(
|
|
60
|
+
[
|
|
61
|
+
[0, 0],
|
|
62
|
+
[1, 3], // After position 0, skip 2 blanks (1,2)
|
|
63
|
+
[2, 4],
|
|
64
|
+
[3, 6], // After position 2, skip 1 blank (5)
|
|
65
|
+
[4, 7],
|
|
66
|
+
] as const
|
|
67
|
+
).forEach(r => {
|
|
68
|
+
expect(mouseOverCoordToGlobalCoord(blanks, r[0])).toBe(r[1])
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('with position exceeding blanks array', () => {
|
|
73
|
+
const blanks = [2, 5]
|
|
74
|
+
;(
|
|
75
|
+
[
|
|
76
|
+
[0, 0],
|
|
77
|
+
[1, 1],
|
|
78
|
+
[2, 3], // After position 1, skip 1 blank (2)
|
|
79
|
+
[3, 4],
|
|
80
|
+
[4, 6], // After position 3, skip 1 blank (5)
|
|
81
|
+
[10, 12], // Far beyond blanks array
|
|
82
|
+
] as const
|
|
83
|
+
).forEach(r => {
|
|
84
|
+
expect(mouseOverCoordToGlobalCoord(blanks, r[0])).toBe(r[1])
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('with gaps in sequence', () => {
|
|
89
|
+
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)
|
|
93
|
+
// Position 3 in global coordinates is after the gap
|
|
94
|
+
expect(globalCoordToRowSpecificCoord(sequence, 3)).toBe(2)
|
|
95
|
+
expect(globalCoordToRowSpecificCoord(sequence, 4)).toBe(3)
|
|
96
|
+
expect(globalCoordToRowSpecificCoord(sequence, 5)).toBe(4)
|
|
97
|
+
// Position 6 in global coordinates is after the gap
|
|
98
|
+
expect(globalCoordToRowSpecificCoord(sequence, 6)).toBe(4)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('with no gaps in sequence', () => {
|
|
102
|
+
const sequence = 'ACGTA'
|
|
103
|
+
expect(globalCoordToRowSpecificCoord(sequence, 0)).toBe(0)
|
|
104
|
+
expect(globalCoordToRowSpecificCoord(sequence, 1)).toBe(1)
|
|
105
|
+
expect(globalCoordToRowSpecificCoord(sequence, 2)).toBe(2)
|
|
106
|
+
expect(globalCoordToRowSpecificCoord(sequence, 3)).toBe(3)
|
|
107
|
+
expect(globalCoordToRowSpecificCoord(sequence, 4)).toBe(4)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('with all gaps in sequence', () => {
|
|
111
|
+
const sequence = '-----'
|
|
112
|
+
expect(globalCoordToRowSpecificCoord(sequence, 0)).toBe(0)
|
|
113
|
+
expect(globalCoordToRowSpecificCoord(sequence, 1)).toBe(0)
|
|
114
|
+
expect(globalCoordToRowSpecificCoord(sequence, 2)).toBe(0)
|
|
115
|
+
expect(globalCoordToRowSpecificCoord(sequence, 3)).toBe(0)
|
|
116
|
+
expect(globalCoordToRowSpecificCoord(sequence, 4)).toBe(0)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('with position exceeding sequence length', () => {
|
|
120
|
+
const sequence = 'AC-GT'
|
|
121
|
+
expect(globalCoordToRowSpecificCoord(sequence, 10)).toBe(4)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('mouseOverCoordToGapRemovedCoord', () => {
|
|
125
|
+
const seq = 'AC--GT--CT'
|
|
126
|
+
expect(
|
|
127
|
+
mouseOverCoordToGapRemovedCoord({ seq, position: 0, blanks: [] }),
|
|
128
|
+
).toBe(0)
|
|
129
|
+
expect(
|
|
130
|
+
mouseOverCoordToGapRemovedCoord({ seq, position: 1, blanks: [] }),
|
|
131
|
+
).toBe(1)
|
|
132
|
+
expect(
|
|
133
|
+
mouseOverCoordToGapRemovedCoord({ seq, position: 2, blanks: [] }),
|
|
134
|
+
).toBe(undefined)
|
|
135
|
+
expect(
|
|
136
|
+
mouseOverCoordToGapRemovedCoord({ seq, position: 3, blanks: [] }),
|
|
137
|
+
).toBe(undefined)
|
|
138
|
+
expect(
|
|
139
|
+
mouseOverCoordToGapRemovedCoord({ seq, position: 4, blanks: [] }),
|
|
140
|
+
).toBe(2)
|
|
141
|
+
expect(
|
|
142
|
+
mouseOverCoordToGapRemovedCoord({ seq, position: 5, blanks: [] }),
|
|
143
|
+
).toBe(3)
|
|
144
|
+
expect(
|
|
145
|
+
mouseOverCoordToGapRemovedCoord({ seq, position: 6, blanks: [] }),
|
|
146
|
+
).toBe(undefined)
|
|
147
|
+
expect(
|
|
148
|
+
mouseOverCoordToGapRemovedCoord({ seq, position: 7, blanks: [] }),
|
|
149
|
+
).toBe(undefined)
|
|
150
|
+
expect(
|
|
151
|
+
mouseOverCoordToGapRemovedCoord({ seq, position: 8, blanks: [] }),
|
|
152
|
+
).toBe(4)
|
|
153
|
+
expect(
|
|
154
|
+
mouseOverCoordToGapRemovedCoord({ seq, position: 9, blanks: [] }),
|
|
155
|
+
).toBe(5)
|
|
156
|
+
})
|
|
@@ -1,26 +1,103 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { isBlank } from './util'
|
|
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)
|
|
10
|
+
const blanksLen = blanks.length
|
|
11
|
+
|
|
12
|
+
// Iterate until we reach the target mouse position
|
|
13
|
+
while (mousePosition < position) {
|
|
14
|
+
// Skip any blank positions in the sequence
|
|
15
|
+
while (
|
|
16
|
+
blankArrayIndex < blanksLen &&
|
|
17
|
+
blanks[blankArrayIndex]! - 1 === globalPosition
|
|
18
|
+
) {
|
|
19
|
+
blankArrayIndex++
|
|
20
|
+
globalPosition++
|
|
11
21
|
}
|
|
22
|
+
|
|
23
|
+
// Move to next position
|
|
24
|
+
mousePosition++
|
|
25
|
+
globalPosition++
|
|
12
26
|
}
|
|
13
|
-
|
|
27
|
+
|
|
28
|
+
return globalPosition
|
|
14
29
|
}
|
|
15
30
|
|
|
16
|
-
export function globalCoordToRowSpecificCoord(seq: string,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
export function globalCoordToRowSpecificCoord(seq: string, position: number) {
|
|
32
|
+
// Initialize counter for non-gap characters
|
|
33
|
+
let nonGapCount = 0
|
|
34
|
+
// Initialize position counter
|
|
35
|
+
let currentPosition = 0
|
|
36
|
+
const sequenceLength = seq.length
|
|
37
|
+
|
|
38
|
+
// Iterate until we reach the target position or end of sequence
|
|
39
|
+
while (currentPosition < position && currentPosition < sequenceLength) {
|
|
40
|
+
// If current character is not a gap, increment the non-gap counter
|
|
41
|
+
if (seq[currentPosition] !== '-') {
|
|
42
|
+
nonGapCount++
|
|
43
|
+
}
|
|
44
|
+
// If we've reached the target position in non-gap coordinates, break
|
|
45
|
+
else if (nonGapCount >= position) {
|
|
22
46
|
break
|
|
23
47
|
}
|
|
48
|
+
currentPosition++
|
|
24
49
|
}
|
|
25
|
-
|
|
50
|
+
|
|
51
|
+
return nonGapCount
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function mouseOverCoordToGapRemovedRowCoord({
|
|
55
|
+
rowName,
|
|
56
|
+
position,
|
|
57
|
+
rowMap,
|
|
58
|
+
blanks,
|
|
59
|
+
}: {
|
|
60
|
+
rowName: string
|
|
61
|
+
position: number
|
|
62
|
+
rowMap: Map<string, string>
|
|
63
|
+
blanks: number[]
|
|
64
|
+
}) {
|
|
65
|
+
const seq = rowMap.get(rowName)
|
|
66
|
+
return seq !== undefined
|
|
67
|
+
? mouseOverCoordToGapRemovedCoord({
|
|
68
|
+
seq,
|
|
69
|
+
position,
|
|
70
|
+
blanks,
|
|
71
|
+
})
|
|
72
|
+
: undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function mouseOverCoordToGapRemovedCoord({
|
|
76
|
+
seq,
|
|
77
|
+
blanks,
|
|
78
|
+
position,
|
|
79
|
+
}: {
|
|
80
|
+
seq: string
|
|
81
|
+
blanks: number[]
|
|
82
|
+
position: number
|
|
83
|
+
}) {
|
|
84
|
+
// First convert the mouse position to global coordinates
|
|
85
|
+
const globalPos = mouseOverCoordToGlobalCoord(blanks, position)
|
|
86
|
+
const seqLen = seq.length
|
|
87
|
+
|
|
88
|
+
// Check if the position in the sequence is a gap
|
|
89
|
+
if (globalPos < seqLen && isBlank(seq[globalPos])) {
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Count non-gap characters up to the global position
|
|
94
|
+
let nonGapCount = 0
|
|
95
|
+
for (let i = 0; i < globalPos && i < seqLen; i++) {
|
|
96
|
+
if (!isBlank(seq[i])) {
|
|
97
|
+
nonGapCount++
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// If we're at a valid position, return the count of non-gap characters
|
|
102
|
+
return globalPos < seqLen ? nonGapCount : undefined
|
|
26
103
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { seqCoordToRowSpecificGlobalCoord } from './seqCoordToRowSpecificGlobalCoord'
|
|
4
|
+
|
|
5
|
+
describe('seqCoordToRowSpecificGlobalCoord', () => {
|
|
6
|
+
test('converts sequence coordinate to global coordinate with no gaps', () => {
|
|
7
|
+
const row = 'ATGCATGC'
|
|
8
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 3 })).toBe(3)
|
|
9
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 0 })).toBe(0)
|
|
10
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 8 })).toBe(8)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('converts sequence coordinate to global coordinate with gaps', () => {
|
|
14
|
+
const row = 'A-TG-CA-TGC'
|
|
15
|
+
// A(0) -(1) T(2) G(3) -(4) C(5) A(6) -(7) T(8) G(9) C(10)
|
|
16
|
+
// Sequence positions: A(0) T(1) G(2) C(3) A(4) T(5) G(6) C(7)
|
|
17
|
+
|
|
18
|
+
// Position 0 (first A) -> Global index 0
|
|
19
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 0 })).toBe(0)
|
|
20
|
+
|
|
21
|
+
// Position 1 (T after first gap) -> Global index 2
|
|
22
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 1 })).toBe(1)
|
|
23
|
+
|
|
24
|
+
// Position 3 (C after second gap) -> Global index 5
|
|
25
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 3 })).toBe(4)
|
|
26
|
+
|
|
27
|
+
// Position 5 (T after third gap) -> Global index 8
|
|
28
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 5 })).toBe(7)
|
|
29
|
+
|
|
30
|
+
// Position 8 (end of sequence) -> Global index 11
|
|
31
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 8 })).toBe(11)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('handles empty row', () => {
|
|
35
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row: '', position: 0 })).toBe(0)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('handles row with only gaps', () => {
|
|
39
|
+
const row = '---..--'
|
|
40
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 0 })).toBe(0)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('handles mixed gap characters', () => {
|
|
44
|
+
const row = 'A-.G-C.'
|
|
45
|
+
// A(0) -(1) .(2) G(3) -(4) C(5) .(6)
|
|
46
|
+
// Sequence positions: A(0) G(1) C(2)
|
|
47
|
+
|
|
48
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 0 })).toBe(0)
|
|
49
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 1 })).toBe(1)
|
|
50
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 2 })).toBe(4)
|
|
51
|
+
expect(seqCoordToRowSpecificGlobalCoord({ row, position: 3 })).toBe(6)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isBlank } from './util'
|
|
2
|
+
|
|
3
|
+
export function seqCoordToRowSpecificGlobalCoord({
|
|
4
|
+
row,
|
|
5
|
+
position,
|
|
6
|
+
}: {
|
|
7
|
+
row: string
|
|
8
|
+
position: number
|
|
9
|
+
}) {
|
|
10
|
+
let k = 0
|
|
11
|
+
let i = 0
|
|
12
|
+
for (; k < position; i++) {
|
|
13
|
+
if (!isBlank(row[i])) {
|
|
14
|
+
k++
|
|
15
|
+
} else if (k >= position) {
|
|
16
|
+
break
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return i
|
|
20
|
+
}
|