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.
Files changed (63) hide show
  1. package/bundle/index.js +15 -15
  2. package/bundle/index.js.LICENSE.txt +5 -13
  3. package/bundle/index.js.map +1 -1
  4. package/dist/colorSchemes.js +2 -2
  5. package/dist/colorSchemes.js.map +1 -1
  6. package/dist/components/VerticalScrollbar.js +2 -2
  7. package/dist/components/VerticalScrollbar.js.map +1 -1
  8. package/dist/components/dialogs/SettingsDialog.js +3 -2
  9. package/dist/components/dialogs/SettingsDialog.js.map +1 -1
  10. package/dist/components/header/Header.js +1 -1
  11. package/dist/components/header/Header.js.map +1 -1
  12. package/dist/components/header/HeaderInfoArea.js +3 -2
  13. package/dist/components/header/HeaderInfoArea.js.map +1 -1
  14. package/dist/components/header/HeaderMenu.js +15 -97
  15. package/dist/components/header/HeaderMenu.js.map +1 -1
  16. package/dist/components/header/SettingsMenu.js +57 -73
  17. package/dist/components/header/SettingsMenu.js.map +1 -1
  18. package/dist/components/header/ZoomMenu.js +14 -2
  19. package/dist/components/header/ZoomMenu.js.map +1 -1
  20. package/dist/components/msa/MSAMouseoverCanvas.js +4 -1
  21. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  22. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +4 -4
  23. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  24. package/dist/components/msa/renderMSABlock.js +13 -9
  25. package/dist/components/msa/renderMSABlock.js.map +1 -1
  26. package/dist/model.d.ts +17 -5
  27. package/dist/model.js +70 -57
  28. package/dist/model.js.map +1 -1
  29. package/dist/rowCoordinateCalculations.d.ts +13 -2
  30. package/dist/rowCoordinateCalculations.js +60 -17
  31. package/dist/rowCoordinateCalculations.js.map +1 -1
  32. package/dist/rowCoordinateCalculations.test.js +96 -2
  33. package/dist/rowCoordinateCalculations.test.js.map +1 -1
  34. package/dist/seqCoordToRowSpecificGlobalCoord.d.ts +4 -0
  35. package/dist/seqCoordToRowSpecificGlobalCoord.js +15 -0
  36. package/dist/seqCoordToRowSpecificGlobalCoord.js.map +1 -0
  37. package/dist/seqCoordToRowSpecificGlobalCoord.test.d.ts +1 -0
  38. package/dist/seqCoordToRowSpecificGlobalCoord.test.js +42 -0
  39. package/dist/seqCoordToRowSpecificGlobalCoord.test.js.map +1 -0
  40. package/dist/util.d.ts +1 -6
  41. package/dist/util.js +5 -22
  42. package/dist/util.js.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/package.json +1 -1
  46. package/src/colorSchemes.ts +2 -2
  47. package/src/components/VerticalScrollbar.tsx +2 -3
  48. package/src/components/dialogs/SettingsDialog.tsx +4 -2
  49. package/src/components/header/Header.tsx +1 -1
  50. package/src/components/header/HeaderInfoArea.tsx +5 -2
  51. package/src/components/header/HeaderMenu.tsx +15 -110
  52. package/src/components/header/SettingsMenu.tsx +64 -81
  53. package/src/components/header/ZoomMenu.tsx +15 -2
  54. package/src/components/msa/MSAMouseoverCanvas.tsx +4 -1
  55. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +4 -4
  56. package/src/components/msa/renderMSABlock.ts +26 -22
  57. package/src/model.ts +89 -67
  58. package/src/rowCoordinateCalculations.test.ts +138 -2
  59. package/src/rowCoordinateCalculations.ts +95 -18
  60. package/src/seqCoordToRowSpecificGlobalCoord.test.ts +53 -0
  61. package/src/seqCoordToRowSpecificGlobalCoord.ts +20 -0
  62. package/src/util.ts +5 -28
  63. package/src/version.ts +1 -1
package/src/model.ts CHANGED
@@ -1,4 +1,12 @@
1
- import { groupBy, notEmpty, sum } from '@jbrowse/core/util'
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
- globalCoordToRowSpecificCoord,
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
- isGzip,
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
- return text
631
- ? reparseTree(
632
- generateNodeIds(
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((item): item is string => !!item)
718
+ .filter(notEmpty)
715
719
  if (strs.length) {
716
- for (let i = 0; i < strs[0]!.length; i++) {
720
+ const s0len = strs[0]!.length
721
+ for (let i = 0; i < s0len; i++) {
717
722
  let counter = 0
718
- for (const str of strs) {
719
- if (str[i] === '-') {
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 / strs.length >= realAllowedGappyness / 100) {
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.fromEntries(
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.maxScrollX, self.scrollX, 0)
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.maxScrollX, self.scrollX, 0)
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.maxScrollX, self.scrollX, 0)
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.maxScrollX, self.scrollX, 0)
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(-self.totalHeight + 10, self.scrollY + deltaY, 0)
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.maxScrollX, self.scrollX + deltaX, 0)
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, n, 0)
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, position: number) {
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
- const { rowMap, blanks } = self
1154
- const seq = rowMap.get(rowName)
1155
- if (seq !== undefined) {
1156
- const pos2 = mouseOverCoordToGlobalCoord(blanks, position)
1157
- const pos1 = globalCoordToRowSpecificCoord(seq, pos2)
1158
- return seq[pos1] === '-' || !seq[pos1] ? undefined : pos1
1159
- } else {
1160
- return undefined
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
- if (index !== -1 && rows[index]) {
1173
- const row = rows[index][1]
1174
-
1175
- let k = 0
1176
- let i = 0
1177
- for (; k < position; i++) {
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({ tree: '', msa: '' })
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
- showEntire() {
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).readFile('utf8'),
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 openLocation(treeMetadataFilehandle).readFile('utf8'),
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
- const res = await openLocation(msaFilehandle).readFile()
1447
- const buf = isGzip(res) ? ungzip(res) : res
1448
- const txt = new TextDecoder('utf8').decode(buf)
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 { mouseOverCoordToGlobalCoord } from './rowCoordinateCalculations'
4
- test('blanks3', () => {
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
- export function mouseOverCoordToGlobalCoord(blanks: number[], pos: number) {
2
- let i = 0 // 'mouse over coord'
3
- let j = 0 // 'position in blanks array'
4
- let k = 0 // 'global coord'/return value
5
-
6
- for (; i < pos; i++, k++) {
7
- // skip multiple gaps in a row
8
- while (j < blanks.length && blanks[j]! - 1 === k) {
9
- j++
10
- k++
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
- return k
27
+
28
+ return globalPosition
14
29
  }
15
30
 
16
- export function globalCoordToRowSpecificCoord(seq: string, pos: number) {
17
- let k = 0
18
- for (let i = 0; i < pos; i++) {
19
- if (seq[i] !== '-') {
20
- k++
21
- } else if (k >= pos) {
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
- return k
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
+ }