react-msaview 4.3.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 (69) 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/dialogs/SettingsDialog.js +1 -1
  9. package/dist/components/dialogs/SettingsDialog.js.map +1 -1
  10. package/dist/components/header/{SettingsMenu.d.ts → ColorMenu.d.ts} +2 -2
  11. package/dist/components/header/ColorMenu.js +19 -0
  12. package/dist/components/header/ColorMenu.js.map +1 -0
  13. package/dist/components/header/Header.js +6 -2
  14. package/dist/components/header/Header.js.map +1 -1
  15. package/dist/components/header/HeaderInfoArea.js +3 -2
  16. package/dist/components/header/HeaderInfoArea.js.map +1 -1
  17. package/dist/components/header/HeaderMenu.js +15 -97
  18. package/dist/components/header/HeaderMenu.js.map +1 -1
  19. package/dist/components/header/MSAMenu.d.ts +6 -0
  20. package/dist/components/header/MSAMenu.js +44 -0
  21. package/dist/components/header/MSAMenu.js.map +1 -0
  22. package/dist/components/header/TreeMenu.d.ts +6 -0
  23. package/dist/components/header/TreeMenu.js +64 -0
  24. package/dist/components/header/TreeMenu.js.map +1 -0
  25. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +4 -4
  26. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  27. package/dist/components/msa/renderMSABlock.js +13 -9
  28. package/dist/components/msa/renderMSABlock.js.map +1 -1
  29. package/dist/model.d.ts +7 -1
  30. package/dist/model.js +54 -53
  31. package/dist/model.js.map +1 -1
  32. package/dist/rowCoordinateCalculations.d.ts +13 -2
  33. package/dist/rowCoordinateCalculations.js +60 -17
  34. package/dist/rowCoordinateCalculations.js.map +1 -1
  35. package/dist/rowCoordinateCalculations.test.js +96 -2
  36. package/dist/rowCoordinateCalculations.test.js.map +1 -1
  37. package/dist/seqCoordToRowSpecificGlobalCoord.d.ts +4 -0
  38. package/dist/seqCoordToRowSpecificGlobalCoord.js +15 -0
  39. package/dist/seqCoordToRowSpecificGlobalCoord.js.map +1 -0
  40. package/dist/seqCoordToRowSpecificGlobalCoord.test.d.ts +1 -0
  41. package/dist/seqCoordToRowSpecificGlobalCoord.test.js +42 -0
  42. package/dist/seqCoordToRowSpecificGlobalCoord.test.js.map +1 -0
  43. package/dist/util.d.ts +1 -6
  44. package/dist/util.js +5 -22
  45. package/dist/util.js.map +1 -1
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/package.json +1 -1
  49. package/src/colorSchemes.ts +2 -2
  50. package/src/components/VerticalScrollbar.tsx +2 -3
  51. package/src/components/dialogs/SettingsDialog.tsx +1 -1
  52. package/src/components/header/ColorMenu.tsx +33 -0
  53. package/src/components/header/Header.tsx +6 -2
  54. package/src/components/header/HeaderInfoArea.tsx +5 -2
  55. package/src/components/header/HeaderMenu.tsx +15 -110
  56. package/src/components/header/MSAMenu.tsx +55 -0
  57. package/src/components/header/TreeMenu.tsx +82 -0
  58. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +4 -4
  59. package/src/components/msa/renderMSABlock.ts +26 -22
  60. package/src/model.ts +73 -61
  61. package/src/rowCoordinateCalculations.test.ts +138 -2
  62. package/src/rowCoordinateCalculations.ts +95 -18
  63. package/src/seqCoordToRowSpecificGlobalCoord.test.ts +53 -0
  64. package/src/seqCoordToRowSpecificGlobalCoord.ts +20 -0
  65. package/src/util.ts +5 -28
  66. package/src/version.ts +1 -1
  67. package/dist/components/header/SettingsMenu.js +0 -141
  68. package/dist/components/header/SettingsMenu.js.map +0 -1
  69. package/src/components/header/SettingsMenu.tsx +0 -169
@@ -0,0 +1,82 @@
1
+ import React from 'react'
2
+
3
+ import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton'
4
+ import AccountTree from '@mui/icons-material/AccountTree'
5
+ import { observer } from 'mobx-react'
6
+
7
+ import type { MsaViewModel } from '../../model'
8
+
9
+ const TreeMenu = observer(({ model }: { model: MsaViewModel }) => {
10
+ const {
11
+ drawTree,
12
+ showBranchLen,
13
+ labelsAlignRight,
14
+ drawNodeBubbles,
15
+ drawLabels,
16
+ treeWidthMatchesArea,
17
+ noTree,
18
+ } = model
19
+ return (
20
+ <CascadingMenuButton
21
+ closeAfterItemClick={false}
22
+ menuItems={[
23
+ {
24
+ label: 'Show branch length',
25
+ type: 'checkbox',
26
+ checked: showBranchLen,
27
+ onClick: () => {
28
+ model.setShowBranchLen(!showBranchLen)
29
+ },
30
+ },
31
+ {
32
+ label: 'Show tree',
33
+ type: 'checkbox',
34
+ checked: drawTree,
35
+ onClick: () => {
36
+ model.setDrawTree(!drawTree)
37
+ },
38
+ },
39
+ {
40
+ label: 'Draw clickable bubbles on tree branches',
41
+ type: 'checkbox',
42
+ checked: drawNodeBubbles,
43
+ onClick: () => {
44
+ model.setDrawNodeBubbles(!drawNodeBubbles)
45
+ },
46
+ },
47
+ {
48
+ label: 'Tree labels align right',
49
+ type: 'checkbox',
50
+ checked: labelsAlignRight,
51
+ onClick: () => {
52
+ model.setLabelsAlignRight(!labelsAlignRight)
53
+ },
54
+ },
55
+ {
56
+ label: 'Draw labels',
57
+ type: 'checkbox',
58
+ checked: drawLabels,
59
+ onClick: () => {
60
+ model.setDrawLabels(!drawLabels)
61
+ },
62
+ },
63
+ ...(noTree
64
+ ? []
65
+ : [
66
+ {
67
+ label: 'Make tree width fit to tree area',
68
+ type: 'checkbox' as const,
69
+ checked: treeWidthMatchesArea,
70
+ onClick: () => {
71
+ model.setTreeWidthMatchesArea(!treeWidthMatchesArea)
72
+ },
73
+ },
74
+ ]),
75
+ ]}
76
+ >
77
+ <AccountTree />
78
+ </CascadingMenuButton>
79
+ )
80
+ })
81
+
82
+ export default TreeMenu
@@ -56,7 +56,8 @@ function drawTiles({
56
56
  tidyFilteredGatheredInterProAnnotations,
57
57
  } = model
58
58
 
59
- for (const node of visibleLeaves) {
59
+ for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
60
+ const node = visibleLeaves[i]!
60
61
  const {
61
62
  x,
62
63
  data: { name },
@@ -65,9 +66,9 @@ function drawTiles({
65
66
 
66
67
  const entry = tidyFilteredGatheredInterProAnnotations[name]
67
68
 
68
- let j = 0
69
69
  if (entry) {
70
- for (const { start, end, accession } of entry) {
70
+ for (let j = 0, l2 = entry.length; j < l2; j++) {
71
+ const { start, end, accession } = entry[j]!
71
72
  const m1 = model.seqCoordToRowSpecificGlobalCoord(name, start - 1)
72
73
  const m2 = model.seqCoordToRowSpecificGlobalCoord(name, end)
73
74
  const x = m1 * colWidth
@@ -78,7 +79,6 @@ function drawTiles({
78
79
  const lw = colWidth * (m2 - m1)
79
80
  ctx.fillRect(x, t, lw, h)
80
81
  ctx.strokeRect(x, t, lw, h)
81
- j++
82
82
  }
83
83
  }
84
84
  }
@@ -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
+ })