react-msaview 4.4.0 → 4.4.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 (66) hide show
  1. package/bundle/index.js +15 -15
  2. package/bundle/index.js.LICENSE.txt +1 -9
  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/header/{SettingsMenu.d.ts → ColorMenu.d.ts} +2 -2
  9. package/dist/components/header/ColorMenu.js +19 -0
  10. package/dist/components/header/ColorMenu.js.map +1 -0
  11. package/dist/components/header/Header.js +6 -2
  12. package/dist/components/header/Header.js.map +1 -1
  13. package/dist/components/header/HeaderInfoArea.js +3 -2
  14. package/dist/components/header/HeaderInfoArea.js.map +1 -1
  15. package/dist/components/header/HeaderMenu.js +15 -97
  16. package/dist/components/header/HeaderMenu.js.map +1 -1
  17. package/dist/components/header/MSAMenu.d.ts +6 -0
  18. package/dist/components/header/MSAMenu.js +44 -0
  19. package/dist/components/header/MSAMenu.js.map +1 -0
  20. package/dist/components/header/TreeMenu.d.ts +6 -0
  21. package/dist/components/header/TreeMenu.js +64 -0
  22. package/dist/components/header/TreeMenu.js.map +1 -0
  23. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +4 -4
  24. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  25. package/dist/components/msa/renderMSABlock.js +13 -9
  26. package/dist/components/msa/renderMSABlock.js.map +1 -1
  27. package/dist/model.d.ts +7 -1
  28. package/dist/model.js +54 -53
  29. package/dist/model.js.map +1 -1
  30. package/dist/rowCoordinateCalculations.d.ts +13 -2
  31. package/dist/rowCoordinateCalculations.js +60 -17
  32. package/dist/rowCoordinateCalculations.js.map +1 -1
  33. package/dist/rowCoordinateCalculations.test.js +96 -2
  34. package/dist/rowCoordinateCalculations.test.js.map +1 -1
  35. package/dist/seqCoordToRowSpecificGlobalCoord.d.ts +4 -0
  36. package/dist/seqCoordToRowSpecificGlobalCoord.js +15 -0
  37. package/dist/seqCoordToRowSpecificGlobalCoord.js.map +1 -0
  38. package/dist/seqCoordToRowSpecificGlobalCoord.test.d.ts +1 -0
  39. package/dist/seqCoordToRowSpecificGlobalCoord.test.js +42 -0
  40. package/dist/seqCoordToRowSpecificGlobalCoord.test.js.map +1 -0
  41. package/dist/util.d.ts +1 -6
  42. package/dist/util.js +5 -22
  43. package/dist/util.js.map +1 -1
  44. package/dist/version.d.ts +1 -1
  45. package/dist/version.js +1 -1
  46. package/package.json +1 -1
  47. package/src/colorSchemes.ts +2 -2
  48. package/src/components/VerticalScrollbar.tsx +2 -3
  49. package/src/components/header/ColorMenu.tsx +33 -0
  50. package/src/components/header/Header.tsx +6 -2
  51. package/src/components/header/HeaderInfoArea.tsx +5 -2
  52. package/src/components/header/HeaderMenu.tsx +15 -110
  53. package/src/components/header/MSAMenu.tsx +55 -0
  54. package/src/components/header/TreeMenu.tsx +82 -0
  55. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +4 -4
  56. package/src/components/msa/renderMSABlock.ts +26 -22
  57. package/src/model.ts +73 -61
  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
  64. package/dist/components/header/SettingsMenu.js +0 -141
  65. package/dist/components/header/SettingsMenu.js.map +0 -1
  66. package/src/components/header/SettingsMenu.tsx +0 -169
@@ -34,6 +34,7 @@ export function renderMSABlock({
34
34
  highResScaleFactor,
35
35
  actuallyShowDomains,
36
36
  leaves,
37
+ bgColor,
37
38
  } = model
38
39
  const k = highResScaleFactorOverride || highResScaleFactor
39
40
  const bx = blockSizeXOverride || blockSize
@@ -42,7 +43,7 @@ export function renderMSABlock({
42
43
  ctx.scale(k, k)
43
44
  ctx.translate(-offsetX, rowHeight / 2 - offsetY)
44
45
  ctx.textAlign = 'center'
45
- ctx.font = ctx.font.replace(/\d+px/, `${fontSize}px`)
46
+ ctx.font = ctx.font.replace(/\d+px/, `${bgColor ? '' : 'bold '}${fontSize}px`)
46
47
 
47
48
  const yStart = Math.max(0, Math.floor((offsetY - rowHeight) / rowHeight))
48
49
  const yEnd = Math.max(0, Math.ceil((offsetY + by + rowHeight) / rowHeight))
@@ -102,38 +103,40 @@ function drawTiles({
102
103
  rowHeight,
103
104
  } = model
104
105
 
105
- for (const node of visibleLeaves) {
106
+ for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
107
+ const node = visibleLeaves[i]!
106
108
  const {
107
109
  data: { name },
108
110
  } = node
109
111
  const y = node.x!
110
112
  const str = columns[name]?.slice(xStart, xEnd)
111
113
  if (str) {
112
- for (let i = 0; i < str.length; i++) {
114
+ for (let i = 0, l2 = str.length; i < l2; i++) {
113
115
  const letter = str[i]!
114
- const color =
115
- colorSchemeName === 'clustalx_protein_dynamic'
116
- ? getClustalXColor(
117
- // use model.colStats dot notation here: delay use of colStats
118
- // until absolutely needed
116
+ const r1 = colorSchemeName === 'clustalx_protein_dynamic'
117
+ const r2 = colorSchemeName === 'percent_identity_dynamic'
118
+ const color = r1
119
+ ? getClustalXColor(
120
+ // use model.colStats dot notation here: delay use of colStats
121
+ // until absolutely needed
122
+ model.colStats[xStart + i]!,
123
+ model.colStatsSums[xStart + i]!,
124
+ model,
125
+ name,
126
+ xStart + i,
127
+ )
128
+ : r2
129
+ ? getPercentIdentityColor(
130
+ // use model.colStats dot notation here: delay use of
131
+ // colStats until absolutely needed
119
132
  model.colStats[xStart + i]!,
120
133
  model.colStatsSums[xStart + i]!,
121
134
  model,
122
135
  name,
123
136
  xStart + i,
124
137
  )
125
- : colorSchemeName === 'percent_identity_dynamic'
126
- ? getPercentIdentityColor(
127
- // use model.colStats dot notation here: delay use of
128
- // colStats until absolutely needed
129
- model.colStats[xStart + i]!,
130
- model.colStatsSums[xStart + i]!,
131
- model,
132
- name,
133
- xStart + i,
134
- )
135
- : colorScheme[letter.toUpperCase()]
136
- if (bgColor) {
138
+ : colorScheme[letter.toUpperCase()]
139
+ if (bgColor || r1 || r2) {
137
140
  ctx.fillStyle = color || theme.palette.background.default
138
141
  ctx.fillRect(
139
142
  i * colWidth + offsetX - (offsetX % colWidth),
@@ -176,14 +179,15 @@ function drawText({
176
179
  rowHeight,
177
180
  } = model
178
181
  if (showMsaLetters) {
179
- for (const node of visibleLeaves) {
182
+ for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
183
+ const node = visibleLeaves[i]!
180
184
  const {
181
185
  data: { name },
182
186
  } = node
183
187
  const y = node.x!
184
188
  const str = columns[name]?.slice(xStart, xEnd)
185
189
  if (str) {
186
- for (let i = 0; i < str.length; i++) {
190
+ for (let i = 0, l2 = str.length; i < l2; i++) {
187
191
  const letter = str[i]!
188
192
  const color = colorScheme[letter.toUpperCase()]
189
193
  const contrast = contrastLettering
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
  }
@@ -948,14 +954,14 @@ function stateModelFactory() {
948
954
  */
949
955
  zoomOutHorizontal() {
950
956
  self.colWidth = Math.max(1, Math.floor(self.colWidth * 0.75))
951
- self.scrollX = clamp(self.maxScrollX, self.scrollX, 0)
957
+ self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
952
958
  },
953
959
  /**
954
960
  * #action
955
961
  */
956
962
  zoomInHorizontal() {
957
963
  self.colWidth = Math.ceil(self.colWidth * 1.5)
958
- self.scrollX = clamp(self.maxScrollX, self.scrollX, 0)
964
+ self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
959
965
  },
960
966
  /**
961
967
  * #action
@@ -976,7 +982,7 @@ function stateModelFactory() {
976
982
  transaction(() => {
977
983
  self.colWidth = Math.ceil(self.colWidth * 1.5)
978
984
  self.rowHeight = Math.ceil(self.rowHeight * 1.5)
979
- self.scrollX = clamp(self.maxScrollX, self.scrollX, 0)
985
+ self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
980
986
  })
981
987
  },
982
988
  /**
@@ -986,7 +992,7 @@ function stateModelFactory() {
986
992
  transaction(() => {
987
993
  self.colWidth = Math.max(1, Math.floor(self.colWidth * 0.75))
988
994
  self.rowHeight = Math.max(1.5, Math.floor(self.rowHeight * 0.75))
989
- self.scrollX = clamp(self.maxScrollX, self.scrollX, 0)
995
+ self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
990
996
  })
991
997
  },
992
998
  /**
@@ -1000,21 +1006,21 @@ function stateModelFactory() {
1000
1006
  * #action
1001
1007
  */
1002
1008
  doScrollY(deltaY: number) {
1003
- self.scrollY = clamp(-self.totalHeight + 10, self.scrollY + deltaY, 0)
1009
+ self.scrollY = clamp(self.scrollY + deltaY, -self.totalHeight + 10, 0)
1004
1010
  },
1005
1011
 
1006
1012
  /**
1007
1013
  * #action
1008
1014
  */
1009
1015
  doScrollX(deltaX: number) {
1010
- self.scrollX = clamp(self.maxScrollX, self.scrollX + deltaX, 0)
1016
+ self.scrollX = clamp(self.scrollX + deltaX, self.maxScrollX, 0)
1011
1017
  },
1012
1018
 
1013
1019
  /**
1014
1020
  * #action
1015
1021
  */
1016
1022
  setScrollX(n: number) {
1017
- self.scrollX = clamp(self.maxScrollX, n, 0)
1023
+ self.scrollX = clamp(n, self.maxScrollX, 0)
1018
1024
  },
1019
1025
 
1020
1026
  /**
@@ -1137,11 +1143,9 @@ function stateModelFactory() {
1137
1143
  * #method
1138
1144
  * return a row-specific letter, or undefined if gap
1139
1145
  */
1140
- mouseOverCoordToRowLetter(rowName: string, position: number) {
1146
+ mouseOverCoordToRowLetter(rowName: string, pos: number) {
1141
1147
  const { rowMap, blanks } = self
1142
- return rowMap.get(rowName)?.[
1143
- mouseOverCoordToGlobalCoord(blanks, position)
1144
- ]
1148
+ return rowMap.get(rowName)?.[mouseOverCoordToGlobalCoord(blanks, pos)]
1145
1149
  },
1146
1150
 
1147
1151
  /**
@@ -1150,15 +1154,25 @@ function stateModelFactory() {
1150
1154
  * global coordinate
1151
1155
  */
1152
1156
  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
- }
1157
+ return mouseOverCoordToGapRemovedRowCoord({
1158
+ rowName,
1159
+ position,
1160
+ rowMap: self.rowMap,
1161
+ blanks: self.blanks,
1162
+ })
1163
+ },
1164
+
1165
+ /**
1166
+ * #method
1167
+ * return a row-specific sequence coordinate, skipping gaps, given a
1168
+ * global coordinate
1169
+ */
1170
+ mouseOverCoordToGapRemovedRowCoordOneBased(
1171
+ rowName: string,
1172
+ position: number,
1173
+ ) {
1174
+ const val = this.mouseOverCoordToGapRemovedRowCoord(rowName, position)
1175
+ return val !== undefined ? val + 1 : undefined
1162
1176
  },
1163
1177
 
1164
1178
  /**
@@ -1169,21 +1183,12 @@ function stateModelFactory() {
1169
1183
  seqCoordToRowSpecificGlobalCoord(rowName: string, position: number) {
1170
1184
  const { rowNames, rows } = self
1171
1185
  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
1186
+ return index !== -1 && rows[index]
1187
+ ? seqCoordToRowSpecificGlobalCoord({
1188
+ row: rows[index][1],
1189
+ position,
1190
+ })
1191
+ : 0
1187
1192
  },
1188
1193
  }))
1189
1194
 
@@ -1321,7 +1326,11 @@ function stateModelFactory() {
1321
1326
  * #action
1322
1327
  */
1323
1328
  reset() {
1324
- self.setData({ tree: '', msa: '' })
1329
+ self.setData({
1330
+ tree: '',
1331
+ msa: '',
1332
+ })
1333
+ self.resetZoom()
1325
1334
  self.setError(undefined)
1326
1335
  self.setScrollY(0)
1327
1336
  self.setScrollX(0)
@@ -1402,7 +1411,7 @@ function stateModelFactory() {
1402
1411
  try {
1403
1412
  self.setLoadingTree(true)
1404
1413
  self.setTree(
1405
- await openLocation(treeFilehandle).readFile('utf8'),
1414
+ await fetchAndMaybeUnzipText(openLocation(treeFilehandle)),
1406
1415
  )
1407
1416
  if (treeFilehandle.locationType === 'BlobLocation') {
1408
1417
  // clear filehandle after loading if from a local file
@@ -1425,7 +1434,9 @@ function stateModelFactory() {
1425
1434
  if (treeMetadataFilehandle) {
1426
1435
  try {
1427
1436
  self.setTreeMetadata(
1428
- await openLocation(treeMetadataFilehandle).readFile('utf8'),
1437
+ await fetchAndMaybeUnzipText(
1438
+ openLocation(treeMetadataFilehandle),
1439
+ ),
1429
1440
  )
1430
1441
  } catch (e) {
1431
1442
  console.error(e)
@@ -1443,9 +1454,10 @@ function stateModelFactory() {
1443
1454
  if (msaFilehandle) {
1444
1455
  try {
1445
1456
  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)
1457
+ self.setError(undefined)
1458
+ const txt = await fetchAndMaybeUnzipText(
1459
+ openLocation(msaFilehandle),
1460
+ )
1449
1461
  transaction(() => {
1450
1462
  self.setMSA(txt)
1451
1463
  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
  }