react-msaview 4.5.0 → 4.8.0

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 (98) hide show
  1. package/bundle/index.js +99 -99
  2. package/bundle/index.js.LICENSE.txt +6 -6
  3. package/bundle/index.js.map +1 -1
  4. package/dist/__snapshots__/parseAsn1.test.js.snap +2400 -0
  5. package/dist/components/header/HeaderInfoArea.js +3 -4
  6. package/dist/components/header/HeaderInfoArea.js.map +1 -1
  7. package/dist/components/import/ImportForm.js +6 -2
  8. package/dist/components/import/ImportForm.js.map +1 -1
  9. package/dist/components/import/util.d.ts +1 -1
  10. package/dist/components/import/util.js +4 -1
  11. package/dist/components/import/util.js.map +1 -1
  12. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +7 -2
  13. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  14. package/dist/components/msa/renderMSABlock.js +20 -18
  15. package/dist/components/msa/renderMSABlock.js.map +1 -1
  16. package/dist/components/msa/renderMSAMouseover.js +8 -1
  17. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  18. package/dist/components/tree/renderTreeCanvas.d.ts +0 -1
  19. package/dist/components/tree/renderTreeCanvas.js +32 -31
  20. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  21. package/dist/model.d.ts +168 -16
  22. package/dist/model.js +116 -29
  23. package/dist/model.js.map +1 -1
  24. package/dist/rowCoordinateCalculations.d.ts +69 -9
  25. package/dist/rowCoordinateCalculations.js +118 -46
  26. package/dist/rowCoordinateCalculations.js.map +1 -1
  27. package/dist/rowCoordinateCalculations.test.js +152 -52
  28. package/dist/rowCoordinateCalculations.test.js.map +1 -1
  29. package/dist/seqPosToGlobalCol.d.ts +19 -0
  30. package/dist/seqPosToGlobalCol.js +34 -0
  31. package/dist/seqPosToGlobalCol.js.map +1 -0
  32. package/dist/seqPosToGlobalCol.test.js +60 -0
  33. package/dist/seqPosToGlobalCol.test.js.map +1 -0
  34. package/dist/util.d.ts +1 -2
  35. package/dist/util.js +0 -9
  36. package/dist/util.js.map +1 -1
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/package.json +7 -9
  40. package/src/components/header/HeaderInfoArea.tsx +2 -5
  41. package/src/components/import/ImportForm.tsx +6 -1
  42. package/src/components/import/util.ts +4 -0
  43. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +7 -2
  44. package/src/components/msa/renderMSABlock.ts +26 -19
  45. package/src/components/msa/renderMSAMouseover.ts +9 -0
  46. package/src/components/tree/renderTreeCanvas.ts +35 -42
  47. package/src/declare.d.ts +0 -1
  48. package/src/model.ts +143 -42
  49. package/src/rowCoordinateCalculations.test.ts +167 -74
  50. package/src/rowCoordinateCalculations.ts +138 -63
  51. package/src/seqPosToGlobalCol.test.ts +71 -0
  52. package/src/seqPosToGlobalCol.ts +40 -0
  53. package/src/util.ts +1 -19
  54. package/src/version.ts +1 -1
  55. package/dist/parseGFF.d.ts +0 -10
  56. package/dist/parseGFF.js +0 -31
  57. package/dist/parseGFF.js.map +0 -1
  58. package/dist/parseNewick.d.ts +0 -60
  59. package/dist/parseNewick.js +0 -95
  60. package/dist/parseNewick.js.map +0 -1
  61. package/dist/parsers/A3mMSA.d.ts +0 -43
  62. package/dist/parsers/A3mMSA.js +0 -277
  63. package/dist/parsers/A3mMSA.js.map +0 -1
  64. package/dist/parsers/A3mMSA.test.js +0 -138
  65. package/dist/parsers/A3mMSA.test.js.map +0 -1
  66. package/dist/parsers/ClustalMSA.d.ts +0 -30
  67. package/dist/parsers/ClustalMSA.js +0 -55
  68. package/dist/parsers/ClustalMSA.js.map +0 -1
  69. package/dist/parsers/EmfMSA.d.ts +0 -27
  70. package/dist/parsers/EmfMSA.js +0 -53
  71. package/dist/parsers/EmfMSA.js.map +0 -1
  72. package/dist/parsers/EmfTree.d.ts +0 -5
  73. package/dist/parsers/EmfTree.js +0 -8
  74. package/dist/parsers/EmfTree.js.map +0 -1
  75. package/dist/parsers/FastaMSA.d.ts +0 -19
  76. package/dist/parsers/FastaMSA.js +0 -69
  77. package/dist/parsers/FastaMSA.js.map +0 -1
  78. package/dist/parsers/StockholmMSA.d.ts +0 -68
  79. package/dist/parsers/StockholmMSA.js +0 -107
  80. package/dist/parsers/StockholmMSA.js.map +0 -1
  81. package/dist/seqCoordToRowSpecificGlobalCoord.d.ts +0 -4
  82. package/dist/seqCoordToRowSpecificGlobalCoord.js +0 -19
  83. package/dist/seqCoordToRowSpecificGlobalCoord.js.map +0 -1
  84. package/dist/seqCoordToRowSpecificGlobalCoord.test.d.ts +0 -1
  85. package/dist/seqCoordToRowSpecificGlobalCoord.test.js +0 -42
  86. package/dist/seqCoordToRowSpecificGlobalCoord.test.js.map +0 -1
  87. package/src/parseGFF.ts +0 -34
  88. package/src/parseNewick.ts +0 -94
  89. package/src/parsers/A3mMSA.test.ts +0 -164
  90. package/src/parsers/A3mMSA.ts +0 -321
  91. package/src/parsers/ClustalMSA.ts +0 -69
  92. package/src/parsers/EmfMSA.ts +0 -67
  93. package/src/parsers/EmfTree.ts +0 -9
  94. package/src/parsers/FastaMSA.ts +0 -82
  95. package/src/parsers/StockholmMSA.ts +0 -140
  96. package/src/seqCoordToRowSpecificGlobalCoord.test.ts +0 -53
  97. package/src/seqCoordToRowSpecificGlobalCoord.ts +0 -25
  98. /package/dist/{parsers/A3mMSA.test.d.ts → seqPosToGlobalCol.test.d.ts} +0 -0
@@ -65,7 +65,6 @@ export function renderMSABlock({
65
65
  ctx,
66
66
  offsetX,
67
67
  contrastScheme,
68
- theme,
69
68
  xStart,
70
69
  xEnd,
71
70
  visibleLeaves,
@@ -112,6 +111,10 @@ function drawTiles({
112
111
  ? columns[relativeTo]?.slice(xStart, xEnd)
113
112
  : null
114
113
 
114
+ const isClustalX = colorSchemeName === 'clustalx_protein_dynamic'
115
+ const isPercentIdentity = colorSchemeName === 'percent_identity_dynamic'
116
+ const offsetXAligned = offsetX - (offsetX % colWidth)
117
+
115
118
  for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
116
119
  const node = visibleLeaves[i]!
117
120
  const {
@@ -120,31 +123,29 @@ function drawTiles({
120
123
  const y = node.x!
121
124
  const str = columns[name]?.slice(xStart, xEnd)
122
125
  if (str) {
123
- for (let i = 0, l2 = str.length; i < l2; i++) {
124
- const letter = str[i]!
126
+ for (let j = 0, l2 = str.length; j < l2; j++) {
127
+ const letter = str[j]!
125
128
 
126
129
  // Use a muted background for positions that match reference
127
130
  const isMatchingReference =
128
- referenceSeq && name !== relativeTo && letter === referenceSeq[i]
131
+ referenceSeq && name !== relativeTo && letter === referenceSeq[j]
129
132
 
130
- const r1 = colorSchemeName === 'clustalx_protein_dynamic'
131
- const r2 = colorSchemeName === 'percent_identity_dynamic'
132
- const color = r1
133
- ? model.colClustalX[xStart + i]![letter]
134
- : r2
133
+ const color = isClustalX
134
+ ? model.colClustalX[xStart + j]![letter]
135
+ : isPercentIdentity
135
136
  ? (() => {
136
- const consensus = model.colConsensus[xStart + i]!
137
+ const consensus = model.colConsensus[xStart + j]!
137
138
  return letter === consensus.letter ? consensus.color : undefined
138
139
  })()
139
140
  : colorScheme[letter.toUpperCase()]
140
- if (bgColor || r1 || r2) {
141
+ if (bgColor || isClustalX || isPercentIdentity) {
141
142
  // Use a very light background for matching positions in relative mode
142
143
  const finalColor = isMatchingReference
143
144
  ? theme.palette.action.hover
144
145
  : color || theme.palette.background.default
145
146
  ctx.fillStyle = finalColor
146
147
  ctx.fillRect(
147
- i * colWidth + offsetX - (offsetX % colWidth),
148
+ j * colWidth + offsetXAligned,
148
149
  y - rowHeight,
149
150
  colWidth,
150
151
  rowHeight,
@@ -167,7 +168,6 @@ function drawText({
167
168
  offsetX: number
168
169
  model: MsaViewModel
169
170
  contrastScheme: Record<string, string>
170
- theme: Theme
171
171
  ctx: CanvasRenderingContext2D
172
172
  visibleLeaves: HierarchyNode<NodeWithIdsAndLength>[]
173
173
  xStart: number
@@ -191,20 +191,24 @@ function drawText({
191
191
  : null
192
192
 
193
193
  if (showMsaLetters) {
194
+ const offsetXAligned = offsetX - (offsetX % colWidth)
195
+ const halfColWidth = colWidth / 2
196
+ const quarterRowHeight = rowHeight / 4
197
+
194
198
  for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
195
199
  const node = visibleLeaves[i]!
196
200
  const {
197
201
  data: { name },
198
202
  } = node
199
- const y = node.x!
203
+ const y = node.x! - quarterRowHeight
200
204
  const str = columns[name]?.slice(xStart, xEnd)
201
205
  if (str) {
202
- for (let i = 0, l2 = str.length; i < l2; i++) {
203
- const letter = str[i]!
206
+ for (let j = 0, l2 = str.length; j < l2; j++) {
207
+ const letter = str[j]!
204
208
 
205
209
  // Check if this position matches the reference
206
210
  const isMatchingReference =
207
- referenceSeq && name !== relativeTo && letter === referenceSeq[i]
211
+ referenceSeq && name !== relativeTo && letter === referenceSeq[j]
208
212
 
209
213
  // Show dot for matching positions, original letter for differences
210
214
  const displayLetter = isMatchingReference ? '.' : letter
@@ -213,7 +217,6 @@ function drawText({
213
217
  const contrast = contrastLettering
214
218
  ? contrastScheme[letter.toUpperCase()] || 'black'
215
219
  : 'black'
216
- const x = i * colWidth + offsetX - (offsetX % colWidth)
217
220
 
218
221
  // note: -rowHeight/4 matches +rowHeight/4 in tree
219
222
  ctx.fillStyle = actuallyShowDomains
@@ -221,7 +224,11 @@ function drawText({
221
224
  : bgColor
222
225
  ? contrast
223
226
  : color || 'black'
224
- ctx.fillText(displayLetter, x + colWidth / 2, y - rowHeight / 4)
227
+ ctx.fillText(
228
+ displayLetter,
229
+ j * colWidth + offsetXAligned + halfColWidth,
230
+ y,
231
+ )
225
232
  }
226
233
  }
227
234
  }
@@ -28,6 +28,7 @@ export function renderMouseover({
28
28
  relativeTo,
29
29
  rowNamesSet,
30
30
  hoveredTreeNode,
31
+ highlightedColumns,
31
32
  } = model
32
33
  ctx.resetTransform()
33
34
  ctx.clearRect(0, 0, width, height)
@@ -52,6 +53,14 @@ export function renderMouseover({
52
53
  }
53
54
  }
54
55
 
56
+ // Highlight multiple columns
57
+ if (highlightedColumns?.length) {
58
+ ctx.fillStyle = highlightColor
59
+ for (const col of highlightedColumns) {
60
+ ctx.fillRect(col * colWidth + scrollX, 0, colWidth, height)
61
+ }
62
+ }
63
+
55
64
  if (mouseCol !== undefined) {
56
65
  ctx.fillStyle = hoverColor
57
66
  ctx.fillRect(mouseCol * colWidth + scrollX, 0, colWidth, height)
@@ -42,15 +42,9 @@ export function renderTree({
42
42
  theme: Theme
43
43
  blockSizeYOverride?: number
44
44
  }) {
45
- const {
46
- hierarchy,
47
- allBranchesLength0,
48
- showBranchLen: showBranchLenPre,
49
- blockSize,
50
- } = model
45
+ const { hierarchy, showBranchLenEffective: showBranchLen, blockSize } = model
51
46
  const by = blockSizeYOverride || blockSize
52
47
  ctx.strokeStyle = theme.palette.text.primary
53
- const showBranchLen = allBranchesLength0 ? false : showBranchLenPre
54
48
  for (const link of hierarchy.links()) {
55
49
  const { source, target } = link
56
50
  if (target.height === 0 && !showBranchLen) {
@@ -58,10 +52,11 @@ export function renderTree({
58
52
  }
59
53
  const sy = source.x!
60
54
  const ty = target.x!
61
- // @ts-expect-error
62
- const tx = showBranchLen ? target.len : target.y
63
- // @ts-expect-error
64
- const sx = showBranchLen ? source.len : source.y
55
+ const tx = showBranchLen ? (target as { len?: number }).len : target.y
56
+ const sx = showBranchLen ? (source as { len?: number }).len : source.y
57
+ if (tx === undefined || sx === undefined) {
58
+ continue
59
+ }
65
60
 
66
61
  const y1 = Math.min(sy, ty)
67
62
  const y2 = Math.max(sy, ty)
@@ -89,23 +84,22 @@ export function renderNodeBubbles({
89
84
  clickMap?: ClickMapIndex
90
85
  offsetY: number
91
86
  model: MsaViewModel
92
- theme: Theme
93
87
  blockSizeYOverride?: number
94
88
  }) {
95
89
  const {
96
90
  hierarchy,
97
- showBranchLen: showBranchLenPre,
98
- allBranchesLength0,
91
+ showBranchLenEffective: showBranchLen,
99
92
  collapsed,
100
93
  blockSize,
101
94
  marginLeft: ml,
102
95
  } = model
103
96
  const by = blockSizeYOverride || blockSize
104
- const showBranchLen = allBranchesLength0 ? false : showBranchLenPre
105
97
  for (const node of hierarchy.descendants()) {
106
- const val = showBranchLen ? 'len' : 'y'
107
- // @ts-expect-error
108
- const { [val]: x, data } = node
98
+ const x = showBranchLen ? (node as { len?: number }).len : node.y
99
+ if (x === undefined) {
100
+ continue
101
+ }
102
+ const { data } = node
109
103
  const y = node.x!
110
104
  const { id, name } = data
111
105
  if (
@@ -150,8 +144,7 @@ export function renderTreeLabels({
150
144
  }) {
151
145
  const {
152
146
  fontSize,
153
- showBranchLen: showBranchLenPre,
154
- allBranchesLength0,
147
+ showBranchLenEffective: showBranchLen,
155
148
  treeMetadata,
156
149
  hierarchy,
157
150
  collapsed,
@@ -167,19 +160,18 @@ export function renderTreeLabels({
167
160
  noTree,
168
161
  } = model
169
162
  const by = blockSizeYOverride || blockSize
163
+ const emHeight = ctx.measureText('M').width
170
164
  if (labelsAlignRight) {
171
165
  ctx.textAlign = 'right'
172
166
  ctx.setLineDash([1, 3])
173
167
  } else {
174
168
  ctx.textAlign = 'start'
175
169
  }
176
- const showBranchLen = allBranchesLength0 ? false : showBranchLenPre
177
170
  for (const node of leaves) {
178
171
  const {
179
172
  data: { name, id },
180
- // @ts-expect-error
181
- len,
182
173
  } = node
174
+ const len = (node as { len?: number }).len
183
175
  const y = node.x!
184
176
  const x = node.y!
185
177
 
@@ -187,28 +179,29 @@ export function renderTreeLabels({
187
179
  if (y > offsetY - extendBounds && y < offsetY + by + extendBounds) {
188
180
  // note: +rowHeight/4 matches with -rowHeight/4 in msa
189
181
  const yp = y + fontSize / 4
190
- let xp = (showBranchLen ? len : x) || 0
191
- if (
192
- !showBranchLen &&
193
- !collapsed.includes(id) &&
194
- !collapsedLeaves.includes(id)
195
- ) {
196
- // this subtraction is a hack to compensate for the leafnode rendering
197
- // glitch (issue #71). the context is that an extra leaf node is added
198
- // so that 'collapsing/hiding leaf nodes is possible' but this causes
199
- // weird workarounds
200
- xp -= treeWidth / hierarchy.height
182
+ let xp = 0
183
+ if (!noTree) {
184
+ xp = (showBranchLen ? len : x) || 0
185
+ if (
186
+ !showBranchLen &&
187
+ !collapsed.includes(id) &&
188
+ !collapsedLeaves.includes(id)
189
+ ) {
190
+ // this subtraction is a hack to compensate for the leafnode rendering
191
+ // glitch (issue #71). the context is that an extra leaf node is added
192
+ // so that 'collapsing/hiding leaf nodes is possible' but this causes
193
+ // weird workarounds
194
+ xp -= treeWidth / hierarchy.height
195
+ }
201
196
  }
202
197
 
203
198
  const { width } = ctx.measureText(displayName)
204
- const height = ctx.measureText('M').width // use an 'em' for height
205
199
 
206
200
  ctx.fillStyle = theme.palette.text.primary
207
201
  if (labelsAlignRight) {
208
202
  const smallPadding = 2
209
203
  const offset = treeAreaWidthMinusMargin - smallPadding
210
204
  if (drawTree && !noTree) {
211
- const { width } = ctx.measureText(displayName)
212
205
  ctx.moveTo(xp + radius + 2, y)
213
206
  ctx.lineTo(offset - smallPadding - width, y)
214
207
  ctx.stroke()
@@ -217,17 +210,18 @@ export function renderTreeLabels({
217
210
  clickMap?.insert({
218
211
  minX: treeAreaWidth - width,
219
212
  maxX: treeAreaWidth,
220
- minY: yp - height,
213
+ minY: yp - emHeight,
221
214
  maxY: yp,
222
215
  name,
223
216
  id,
224
217
  })
225
218
  } else {
226
- ctx.fillText(displayName, xp + d, yp)
219
+ const labelX = noTree ? 2 : xp + d
220
+ ctx.fillText(displayName, labelX, yp)
227
221
  clickMap?.insert({
228
- minX: xp + d + marginLeft,
229
- maxX: xp + d + width + marginLeft,
230
- minY: yp - height,
222
+ minX: labelX + marginLeft,
223
+ maxX: labelX + width + marginLeft,
224
+ minY: yp - emHeight,
231
225
  maxY: yp,
232
226
  name,
233
227
  id,
@@ -303,7 +297,6 @@ export function renderTreeCanvas({
303
297
  offsetY,
304
298
  clickMap,
305
299
  model,
306
- theme,
307
300
  blockSizeYOverride,
308
301
  })
309
302
  }
package/src/declare.d.ts CHANGED
@@ -1,2 +1 @@
1
- declare module 'stockholm-js'
2
1
  declare module 'svgcanvas'
package/src/model.ts CHANGED
@@ -9,14 +9,25 @@ import {
9
9
  } from '@jbrowse/core/util'
10
10
  import { openLocation } from '@jbrowse/core/util/io'
11
11
  import { ElementId, FileLocation } from '@jbrowse/core/util/types/mst'
12
+ import {
13
+ A3mMSA,
14
+ ClustalMSA,
15
+ EmfMSA,
16
+ FastaMSA,
17
+ StockholmMSA,
18
+ generateNodeIds,
19
+ gffToInterProResults,
20
+ parseEmfTree,
21
+ parseGFF,
22
+ parseNewick,
23
+ stockholmSniff,
24
+ } from '@react-msaview/parsers'
12
25
  import { colord } from 'colord'
13
26
  import { ascending } from 'd3-array'
14
27
  import { cluster, hierarchy } from 'd3-hierarchy'
15
- import { parseEmfTree } from 'emf-js'
16
28
  import { saveAs } from 'file-saver'
17
29
  import { autorun, transaction } from 'mobx'
18
30
  import { addDisposer, cast, types } from 'mobx-state-tree'
19
- import Stockholm from 'stockholm-js'
20
31
 
21
32
  import { blocksX, blocksY } from './calculateBlocks'
22
33
  import colorSchemes from './colorSchemes'
@@ -55,26 +66,14 @@ import { MSAModelF } from './model/msaModel'
55
66
  import { TreeModelF } from './model/treeModel'
56
67
  import { calculateNeighborJoiningTree } from './neighborJoining'
57
68
  import { parseAsn1 } from './parseAsn1'
58
- import parseNewick from './parseNewick'
59
- import A3mMSA from './parsers/A3mMSA'
60
- import ClustalMSA from './parsers/ClustalMSA'
61
- import EmfMSA from './parsers/EmfMSA'
62
- import FastaMSA from './parsers/FastaMSA'
63
- import StockholmMSA from './parsers/StockholmMSA'
64
69
  import { reparseTree } from './reparseTree'
65
70
  import {
66
- mouseOverCoordToGapRemovedRowCoord,
67
- mouseOverCoordToGlobalCoord,
71
+ globalColToVisibleCol,
72
+ visibleColToGlobalCol,
73
+ visibleColToSeqPosForRow,
68
74
  } from './rowCoordinateCalculations'
69
- import { seqCoordToRowSpecificGlobalCoord } from './seqCoordToRowSpecificGlobalCoord'
70
- import {
71
- collapse,
72
- generateNodeIds,
73
- len,
74
- maxLength,
75
- setBrLength,
76
- skipBlanks,
77
- } from './util'
75
+ import { seqPosToGlobalCol } from './seqPosToGlobalCol'
76
+ import { collapse, len, maxLength, setBrLength, skipBlanks } from './util'
78
77
 
79
78
  import type { InterProScanResults } from './launchInterProScan'
80
79
  import type {
@@ -193,6 +192,12 @@ function stateModelFactory() {
193
192
  */
194
193
  treeMetadataFilehandle: types.maybe(FileLocation),
195
194
 
195
+ /**
196
+ * #property
197
+ * filehandle object for InterProScan GFF file
198
+ */
199
+ gffFilehandle: types.maybe(FileLocation),
200
+
196
201
  /**
197
202
  * #property
198
203
  *
@@ -321,6 +326,12 @@ function stateModelFactory() {
321
326
  | { nodeId: string; descendantNames: string[] }
322
327
  | undefined,
323
328
 
329
+ /**
330
+ * #volatile
331
+ * array of column indices to highlight
332
+ */
333
+ highlightedColumns: undefined as number[] | undefined,
334
+
324
335
  /**
325
336
  * #volatile
326
337
  * a dummy variable that is incremented when ref changes so autorun for
@@ -453,6 +464,13 @@ function stateModelFactory() {
453
464
 
454
465
  self.hoveredTreeNode = { nodeId, descendantNames }
455
466
  },
467
+ /**
468
+ * #action
469
+ * set highlighted columns
470
+ */
471
+ setHighlightedColumns(columns?: number[]) {
472
+ self.highlightedColumns = columns
473
+ },
456
474
  /**
457
475
  * #action
458
476
  */
@@ -556,6 +574,13 @@ function stateModelFactory() {
556
574
  self.treeFilehandle = treeFilehandle
557
575
  },
558
576
 
577
+ /**
578
+ * #action
579
+ */
580
+ setGFFFilehandle(gffFilehandle?: FileLocationType) {
581
+ self.gffFilehandle = gffFilehandle
582
+ },
583
+
559
584
  /**
560
585
  * #action
561
586
  */
@@ -677,7 +702,7 @@ function stateModelFactory() {
677
702
  get MSA() {
678
703
  const text = self.data.msa
679
704
  if (text) {
680
- if (Stockholm.sniff(text)) {
705
+ if (stockholmSniff(text)) {
681
706
  return new StockholmMSA(text, self.currentAlignment)
682
707
  } else if (A3mMSA.sniff(text)) {
683
708
  return new A3mMSA(text)
@@ -1217,6 +1242,14 @@ function stateModelFactory() {
1217
1242
  get allBranchesLength0() {
1218
1243
  return this.hierarchy.links().every(s => !s.source.data.length)
1219
1244
  },
1245
+
1246
+ /**
1247
+ * #getter
1248
+ * effective showBranchLen accounting for allBranchesLength0
1249
+ */
1250
+ get showBranchLenEffective() {
1251
+ return this.allBranchesLength0 ? false : self.showBranchLen
1252
+ },
1220
1253
  }))
1221
1254
  .views(self => ({
1222
1255
  /**
@@ -1464,7 +1497,7 @@ function stateModelFactory() {
1464
1497
  .map(t => ({
1465
1498
  model: {
1466
1499
  ...t,
1467
- data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data,
1500
+ data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data!,
1468
1501
  height: rowHeight,
1469
1502
  } as TextTrackModel,
1470
1503
  ReactComponent: TextTrack,
@@ -1510,22 +1543,30 @@ function stateModelFactory() {
1510
1543
 
1511
1544
  /**
1512
1545
  * #method
1513
- * return a row-specific letter, or undefined if gap
1546
+ * Return a row-specific letter at a visible column, or undefined if gap.
1547
+ *
1548
+ * @param rowName - The name of the row
1549
+ * @param visibleCol - The visible column index (what the user sees on screen)
1550
+ * @returns The letter at that position, or undefined if it's a gap
1514
1551
  */
1515
- mouseOverCoordToRowLetter(rowName: string, pos: number) {
1552
+ visibleColToRowLetter(rowName: string, visibleCol: number) {
1516
1553
  const { rowMap, blanks } = self
1517
- return rowMap.get(rowName)?.[mouseOverCoordToGlobalCoord(blanks, pos)]
1554
+ return rowMap.get(rowName)?.[visibleColToGlobalCol(blanks, visibleCol)]
1518
1555
  },
1519
1556
 
1520
1557
  /**
1521
1558
  * #method
1522
- * return a row-specific sequence coordinate, skipping gaps, given a
1523
- * global coordinate
1559
+ * Convert a visible column to a row-specific sequence position (0-based).
1560
+ * Returns undefined if the position is a gap in the sequence.
1561
+ *
1562
+ * @param rowName - The name of the row
1563
+ * @param visibleCol - The visible column index
1564
+ * @returns The sequence position (0-based), or undefined if it's a gap
1524
1565
  */
1525
- mouseOverCoordToGapRemovedRowCoord(rowName: string, position: number) {
1526
- return mouseOverCoordToGapRemovedRowCoord({
1566
+ visibleColToSeqPos(rowName: string, visibleCol: number) {
1567
+ return visibleColToSeqPosForRow({
1527
1568
  rowName,
1528
- position,
1569
+ visibleCol,
1529
1570
  rowMap: self.rowMap,
1530
1571
  blanks: self.blanks,
1531
1572
  })
@@ -1533,32 +1574,67 @@ function stateModelFactory() {
1533
1574
 
1534
1575
  /**
1535
1576
  * #method
1536
- * return a row-specific sequence coordinate, skipping gaps, given a
1537
- * global coordinate
1538
- */
1539
- mouseOverCoordToGapRemovedRowCoordOneBased(
1540
- rowName: string,
1541
- position: number,
1542
- ) {
1543
- const val = this.mouseOverCoordToGapRemovedRowCoord(rowName, position)
1577
+ * Convert a visible column to a row-specific sequence position (1-based).
1578
+ * Returns undefined if the position is a gap in the sequence.
1579
+ *
1580
+ * @param rowName - The name of the row
1581
+ * @param visibleCol - The visible column index
1582
+ * @returns The sequence position (1-based), or undefined if it's a gap
1583
+ */
1584
+ visibleColToSeqPosOneBased(rowName: string, visibleCol: number) {
1585
+ const val = this.visibleColToSeqPos(rowName, visibleCol)
1544
1586
  return val !== undefined ? val + 1 : undefined
1545
1587
  },
1546
1588
 
1547
1589
  /**
1548
1590
  * #method
1549
- * return a global coordinate given a row-specific sequence coordinate
1550
- * which does not not include gaps
1591
+ * Convert a global column index to a visible column index.
1592
+ * Returns undefined if the column is hidden (in blanks).
1593
+ * This is the inverse of visibleColToGlobalCol.
1594
+ *
1595
+ * @param globalCol - The global column index in the full MSA
1596
+ * @returns The visible column index, or undefined if the column is hidden
1551
1597
  */
1552
- seqCoordToRowSpecificGlobalCoord(rowName: string, position: number) {
1598
+ globalColToVisibleCol(globalCol: number) {
1599
+ const { blanks, hideGapsEffective } = self
1600
+ if (!hideGapsEffective) {
1601
+ return globalCol
1602
+ }
1603
+ return globalColToVisibleCol(blanks, globalCol)
1604
+ },
1605
+
1606
+ /**
1607
+ * #method
1608
+ * Convert a sequence position (ungapped) to a global column index.
1609
+ *
1610
+ * @param rowName - The name of the row
1611
+ * @param seqPos - The sequence position (0-based, ungapped)
1612
+ * @returns The global column index in the full MSA
1613
+ */
1614
+ seqPosToGlobalCol(rowName: string, seqPos: number) {
1553
1615
  const { rowNames, rows } = self
1554
1616
  const index = rowNames.indexOf(rowName)
1555
1617
  return index !== -1 && rows[index]
1556
- ? seqCoordToRowSpecificGlobalCoord({
1618
+ ? seqPosToGlobalCol({
1557
1619
  row: rows[index][1],
1558
- position,
1620
+ seqPos,
1559
1621
  })
1560
1622
  : 0
1561
1623
  },
1624
+
1625
+ /**
1626
+ * #method
1627
+ * Convert a sequence position (ungapped) directly to a visible column index.
1628
+ * This combines seqPosToGlobalCol and globalColToVisibleCol.
1629
+ *
1630
+ * @param rowName - The name of the row
1631
+ * @param seqPos - The sequence position (0-based, ungapped)
1632
+ * @returns The visible column index, or undefined if the column is hidden
1633
+ */
1634
+ seqPosToVisibleCol(rowName: string, seqPos: number) {
1635
+ const globalCol = this.seqPosToGlobalCol(rowName, seqPos)
1636
+ return this.globalColToVisibleCol(globalCol)
1637
+ },
1562
1638
  }))
1563
1639
 
1564
1640
  .views(self => ({
@@ -1830,6 +1906,31 @@ function stateModelFactory() {
1830
1906
  }),
1831
1907
  )
1832
1908
 
1909
+ // autorun opens gffFilehandle for InterProScan domains
1910
+ addDisposer(
1911
+ self,
1912
+ autorun(async () => {
1913
+ const { gffFilehandle } = self
1914
+ if (gffFilehandle) {
1915
+ try {
1916
+ const gffText = await fetchAndMaybeUnzipText(
1917
+ openLocation(gffFilehandle),
1918
+ )
1919
+ const gffRecords = parseGFF(gffText)
1920
+ const interProResults = gffToInterProResults(gffRecords)
1921
+ self.setInterProAnnotations(interProResults)
1922
+ self.setShowDomains(true)
1923
+ if (gffFilehandle.locationType === 'BlobLocation') {
1924
+ self.setGFFFilehandle(undefined)
1925
+ }
1926
+ } catch (e) {
1927
+ console.error(e)
1928
+ self.setError(e)
1929
+ }
1930
+ }
1931
+ }),
1932
+ )
1933
+
1833
1934
  // autorun opens msaFilehandle
1834
1935
  addDisposer(
1835
1936
  self,