react-msaview 5.3.0 → 5.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 (57) hide show
  1. package/bundle/index.js +22 -22
  2. package/bundle/index.js.LICENSE.txt +1 -1
  3. package/bundle/index.js.map +1 -1
  4. package/dist/components/Checkbox2.js +3 -1
  5. package/dist/components/Checkbox2.js.map +1 -1
  6. package/dist/components/Loading.js +6 -4
  7. package/dist/components/Loading.js.map +1 -1
  8. package/dist/components/MSAViewer.d.ts +3 -1
  9. package/dist/components/MSAViewer.js +3 -4
  10. package/dist/components/MSAViewer.js.map +1 -1
  11. package/dist/components/dialogs/InterProScanDialog.js +2 -1
  12. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  13. package/dist/components/msa/renderMSABlock.js +4 -6
  14. package/dist/components/msa/renderMSABlock.js.map +1 -1
  15. package/dist/components/renderCtx.d.ts +1 -1
  16. package/dist/components/tracks/renderTracksSvg.js +1 -1
  17. package/dist/components/tracks/renderTracksSvg.js.map +1 -1
  18. package/dist/components/tree/TreeNodeMenu.js +1 -1
  19. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  20. package/dist/components/tree/renderTreeCanvas.d.ts +10 -0
  21. package/dist/components/tree/renderTreeCanvas.js +84 -12
  22. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  23. package/dist/fetchUtils.d.ts +19 -0
  24. package/dist/fetchUtils.js +28 -0
  25. package/dist/fetchUtils.js.map +1 -1
  26. package/dist/hierarchy.d.ts +9 -1
  27. package/dist/hierarchy.js +37 -0
  28. package/dist/hierarchy.js.map +1 -1
  29. package/dist/model/msaModel.d.ts +7 -0
  30. package/dist/model/msaModel.js +14 -0
  31. package/dist/model/msaModel.js.map +1 -1
  32. package/dist/model.d.ts +9 -4
  33. package/dist/model.js +95 -32
  34. package/dist/model.js.map +1 -1
  35. package/dist/neighborJoining.js +44 -30
  36. package/dist/neighborJoining.js.map +1 -1
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/package.json +5 -5
  40. package/src/components/Checkbox2.tsx +7 -1
  41. package/src/components/Loading.tsx +15 -5
  42. package/src/components/MSAViewer.tsx +5 -3
  43. package/src/components/dialogs/InterProScanDialog.tsx +2 -1
  44. package/src/components/msa/renderMSABlock.ts +2 -8
  45. package/src/components/renderCtx.ts +1 -0
  46. package/src/components/tracks/renderTracksSvg.ts +1 -1
  47. package/src/components/tree/TreeNodeMenu.tsx +1 -1
  48. package/src/components/tree/renderTreeCanvas.ts +117 -11
  49. package/src/fetchUtils.test.ts +66 -0
  50. package/src/fetchUtils.ts +48 -0
  51. package/src/hierarchy.test.ts +56 -0
  52. package/src/hierarchy.ts +44 -1
  53. package/src/impgRender.test.tsx +125 -0
  54. package/src/model/msaModel.ts +21 -0
  55. package/src/model.ts +99 -55
  56. package/src/neighborJoining.ts +44 -31
  57. package/src/version.ts +1 -1
package/src/model.ts CHANGED
@@ -1,10 +1,4 @@
1
- import {
2
- clamp,
3
- fetchAndMaybeUnzipText,
4
- groupBy,
5
- notEmpty,
6
- sum,
7
- } from '@jbrowse/core/util'
1
+ import { clamp, groupBy, notEmpty, sum } from '@jbrowse/core/util'
8
2
  import { openLocation } from '@jbrowse/core/util/io'
9
3
  import { ElementId, FileLocation } from '@jbrowse/core/util/types/mst'
10
4
  import { addDisposer, cast, types } from '@jbrowse/mobx-state-tree'
@@ -51,13 +45,16 @@ import {
51
45
  minRowHeight,
52
46
  } from './constants.ts'
53
47
  import { createPaletteMap } from './createPaletteMap.ts'
48
+ import { fetchTextWithProgress, isAbortError } from './fetchUtils.ts'
54
49
  import { flatToTree } from './flatToTree.ts'
55
50
  import {
56
51
  calcDepthToLeaf,
57
52
  clusterLayout,
58
53
  collapse,
54
+ collapsedSubtreeLengthExtent,
59
55
  find,
60
56
  findMaxBranchLen,
57
+ forEachDescendant,
61
58
  hierarchy,
62
59
  leaves,
63
60
  links,
@@ -252,6 +249,15 @@ function stateModelFactory() {
252
249
  * #property
253
250
  */
254
251
  relativeTo: types.maybe(types.string),
252
+ /**
253
+ * #property
254
+ * declarative seed for the highlighted-columns overlay (visible column
255
+ * indices). Unlike the volatile `highlightedColumns` (driven by
256
+ * transient genome-hover sync), this persists in the snapshot/URL so a
257
+ * shared link can open with specific columns highlighted. Applied once
258
+ * in afterCreate.
259
+ */
260
+ highlightColumns: types.frozen<number[] | undefined>(),
255
261
  }),
256
262
  )
257
263
  .volatile(() => ({
@@ -669,7 +675,9 @@ function stateModelFactory() {
669
675
  const text = self.data.msa
670
676
  // uses parseMSA so the named MSAParserType return type is portable
671
677
  // to downstream consumers (avoids TS2883 with default exports)
672
- return text ? parseMSA(text, self.currentAlignment) : null
678
+ return text
679
+ ? parseMSA(text, self.currentAlignment, self.msaFormat)
680
+ : null
673
681
  },
674
682
  /**
675
683
  * #getter
@@ -945,7 +953,10 @@ function stateModelFactory() {
945
953
  get colConsensus() {
946
954
  const { colStats, colStatsSums } = this
947
955
  return colStats.map((stats, i) => {
948
- const total = colStatsSums[i]!
956
+ const total = colStatsSums[i]
957
+ if (!total) {
958
+ return { letter: '', color: undefined }
959
+ }
949
960
  let maxCount = 0
950
961
  let letter = ''
951
962
  for (const [key, val] of Object.entries(stats)) {
@@ -1023,7 +1034,18 @@ function stateModelFactory() {
1023
1034
  clusterLayout(r, this.totalHeight, self.treeWidth)
1024
1035
  r.data.length = 0
1025
1036
  const max = maxLength(r)
1026
- setBrLength(r, 0, max ? self.treeWidth / max : 0)
1037
+ const k = max ? self.treeWidth / max : 0
1038
+ setBrLength(r, 0, k)
1039
+ // for each collapsed clade, record the pixel x-positions of its
1040
+ // nearest/farthest tips so the renderer can draw a triangle that spans
1041
+ // the branch-length extent of the hidden subtree
1042
+ forEachDescendant(r, node => {
1043
+ if (node._children) {
1044
+ const { min, max: mx } = collapsedSubtreeLengthExtent(node)
1045
+ node.collapsedTipXNear = (node.len ?? 0) + min * k
1046
+ node.collapsedTipXFar = (node.len ?? 0) + mx * k
1047
+ }
1048
+ })
1027
1049
  return r as HierarchyNode<NodeWithIdsAndLength>
1028
1050
  },
1029
1051
 
@@ -1249,12 +1271,8 @@ function stateModelFactory() {
1249
1271
  */
1250
1272
  zoomIn() {
1251
1273
  transaction(() => {
1252
- self.colWidth = Math.min(maxCellSize, Math.ceil(self.colWidth * 1.5))
1253
- self.rowHeight = Math.min(
1254
- maxCellSize,
1255
- Math.ceil(self.rowHeight * 1.5),
1256
- )
1257
- self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
1274
+ this.zoomInHorizontal()
1275
+ this.zoomInVertical()
1258
1276
  })
1259
1277
  },
1260
1278
  /**
@@ -1262,15 +1280,8 @@ function stateModelFactory() {
1262
1280
  */
1263
1281
  zoomOut() {
1264
1282
  transaction(() => {
1265
- self.colWidth = Math.max(
1266
- minColWidth,
1267
- Math.floor(self.colWidth * 0.75),
1268
- )
1269
- self.rowHeight = Math.max(
1270
- minRowHeight,
1271
- Math.floor(self.rowHeight * 0.75),
1272
- )
1273
- self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
1283
+ this.zoomOutHorizontal()
1284
+ this.zoomOutVertical()
1274
1285
  })
1275
1286
  },
1276
1287
  /**
@@ -1304,9 +1315,13 @@ function stateModelFactory() {
1304
1315
  )
1305
1316
 
1306
1317
  const anchoredScrollY = offsetY - rowInView * self.rowHeight
1307
- const overflow = Math.max(0, self.totalHeight - self.height)
1318
+ // maxScrollY is -(totalHeight - visibleMsaHeight) when the alignment
1319
+ // overflows, so -maxScrollY is exactly that overflow past the
1320
+ // scrollable MSA viewport (0 when it fits)
1321
+ const overflow = Math.max(0, -self.maxScrollY)
1322
+ const visibleHeight = self.totalHeight - overflow
1308
1323
  const topBias =
1309
- self.height > 0 ? clamp(1 - overflow / self.height, 0, 1) : 0
1324
+ visibleHeight > 0 ? clamp(1 - overflow / visibleHeight, 0, 1) : 1
1310
1325
  self.scrollY = clamp(
1311
1326
  anchoredScrollY * (1 - topBias),
1312
1327
  self.maxScrollY,
@@ -1693,22 +1708,8 @@ function stateModelFactory() {
1693
1708
  * #action
1694
1709
  */
1695
1710
  fit() {
1696
- if (self.numRows > 0) {
1697
- self.rowHeight = clamp(
1698
- self.msaAreaHeight / self.numRows,
1699
- minRowHeight,
1700
- maxCellSize,
1701
- )
1702
- }
1703
- if (self.numColumns > 0) {
1704
- self.colWidth = clamp(
1705
- self.msaAreaWidth / self.numColumns,
1706
- minColWidth,
1707
- maxCellSize,
1708
- )
1709
- }
1710
- self.scrollX = 0
1711
- self.scrollY = 0
1711
+ this.fitVertically()
1712
+ this.fitHorizontally()
1712
1713
  },
1713
1714
  /**
1714
1715
  * #action
@@ -1738,6 +1739,13 @@ function stateModelFactory() {
1738
1739
  },
1739
1740
 
1740
1741
  afterCreate() {
1742
+ // seed the highlighted-columns overlay from the declarative property so
1743
+ // a shared snapshot/URL opens with those columns highlighted (the
1744
+ // volatile highlightedColumns can later be driven by genome-hover sync)
1745
+ if (self.highlightColumns?.length) {
1746
+ self.setHighlightedColumns(self.highlightColumns)
1747
+ }
1748
+
1741
1749
  addDisposer(
1742
1750
  self,
1743
1751
  autorun(() => {
@@ -1759,8 +1767,13 @@ function stateModelFactory() {
1759
1767
  const generation = ++treeGeneration
1760
1768
  try {
1761
1769
  self.setLoadingTree(true)
1762
- const text = await fetchAndMaybeUnzipText(
1770
+ const text = await fetchTextWithProgress(
1763
1771
  openLocation(treeFilehandle),
1772
+ status => {
1773
+ if (generation === treeGeneration) {
1774
+ self.setStatus(status)
1775
+ }
1776
+ },
1764
1777
  )
1765
1778
  if (generation === treeGeneration) {
1766
1779
  transaction(() => {
@@ -1773,8 +1786,14 @@ function stateModelFactory() {
1773
1786
  }
1774
1787
  } catch (e) {
1775
1788
  if (generation === treeGeneration) {
1776
- console.error(e)
1777
- self.setError(e)
1789
+ if (isAbortError(e)) {
1790
+ // cancelled by the user: drop the filehandle so the view
1791
+ // returns to the import form instead of a stuck spinner
1792
+ self.setTreeFilehandle(undefined)
1793
+ } else {
1794
+ console.error(e)
1795
+ self.setError(e)
1796
+ }
1778
1797
  }
1779
1798
  } finally {
1780
1799
  if (generation === treeGeneration) {
@@ -1792,13 +1811,19 @@ function stateModelFactory() {
1792
1811
  if (treeMetadataFilehandle) {
1793
1812
  try {
1794
1813
  self.setTreeMetadata(
1795
- await fetchAndMaybeUnzipText(
1814
+ await fetchTextWithProgress(
1796
1815
  openLocation(treeMetadataFilehandle),
1816
+ status => {
1817
+ self.setStatus(status)
1818
+ },
1797
1819
  ),
1798
1820
  )
1799
1821
  } catch (e) {
1800
- console.error(e)
1801
- self.setError(e)
1822
+ // ignore user cancel (isAbortError); treeMetadata is optional
1823
+ if (!isAbortError(e)) {
1824
+ console.error(e)
1825
+ self.setError(e)
1826
+ }
1802
1827
  }
1803
1828
  }
1804
1829
  }),
@@ -1827,16 +1852,24 @@ function stateModelFactory() {
1827
1852
  const { gffFilehandle } = self
1828
1853
  if (gffFilehandle) {
1829
1854
  try {
1830
- const gffText = await fetchAndMaybeUnzipText(
1855
+ const gffText = await fetchTextWithProgress(
1831
1856
  openLocation(gffFilehandle),
1857
+ status => {
1858
+ self.setStatus(status)
1859
+ },
1832
1860
  )
1833
1861
  self.applyGFFText(gffText)
1834
1862
  if (gffFilehandle.locationType === 'BlobLocation') {
1835
1863
  self.setGFFFilehandle(undefined)
1836
1864
  }
1837
1865
  } catch (e) {
1838
- console.error(e)
1839
- self.setError(e)
1866
+ // on user cancel (isAbortError) drop the filehandle
1867
+ if (isAbortError(e)) {
1868
+ self.setGFFFilehandle(undefined)
1869
+ } else {
1870
+ console.error(e)
1871
+ self.setError(e)
1872
+ }
1840
1873
  }
1841
1874
  }
1842
1875
  }),
@@ -1853,8 +1886,13 @@ function stateModelFactory() {
1853
1886
  try {
1854
1887
  self.setLoadingMSA(true)
1855
1888
  self.setError(undefined)
1856
- const txt = await fetchAndMaybeUnzipText(
1889
+ const txt = await fetchTextWithProgress(
1857
1890
  openLocation(msaFilehandle),
1891
+ status => {
1892
+ if (generation === msaGeneration) {
1893
+ self.setStatus(status)
1894
+ }
1895
+ },
1858
1896
  )
1859
1897
  if (generation === msaGeneration) {
1860
1898
  transaction(() => {
@@ -1867,8 +1905,14 @@ function stateModelFactory() {
1867
1905
  }
1868
1906
  } catch (e) {
1869
1907
  if (generation === msaGeneration) {
1870
- console.error(e)
1871
- self.setError(e)
1908
+ if (isAbortError(e)) {
1909
+ // cancelled by the user: drop the filehandle so the view
1910
+ // returns to the import form instead of a stuck spinner
1911
+ self.setMSAFilehandle(undefined)
1912
+ } else {
1913
+ console.error(e)
1914
+ self.setError(e)
1915
+ }
1872
1916
  }
1873
1917
  } finally {
1874
1918
  if (generation === msaGeneration) {
@@ -194,16 +194,10 @@ function neighborJoining(distances: number[][], names: string[]): NJNode {
194
194
  const ri = r.get(minI)!
195
195
  const rj = r.get(minJ)!
196
196
 
197
- let limbI: number
198
- let limbJ: number
199
-
200
- if (remaining > 2) {
201
- limbI = dij / 2 + (ri - rj) / (2 * (remaining - 2))
202
- limbJ = dij - limbI
203
- } else {
204
- limbI = dij / 2
205
- limbJ = dij / 2
206
- }
197
+ // remaining is always > 2 inside this loop; the final two nodes are
198
+ // connected after it
199
+ let limbI = dij / 2 + (ri - rj) / (2 * (remaining - 2))
200
+ let limbJ = dij - limbI
207
201
 
208
202
  // Ensure non-negative branch lengths
209
203
  limbI = Math.max(0, limbI)
@@ -217,19 +211,18 @@ function neighborJoining(distances: number[][], names: string[]): NJNode {
217
211
  rightLength: limbJ,
218
212
  }
219
213
 
220
- // Update distance matrix
221
- const newIdx = minI
214
+ // Update distance matrix, reusing minI's slot for the merged node
222
215
  for (const k of active) {
223
216
  if (k !== minI && k !== minJ) {
224
217
  const newDist = (D[minI]![k]! + D[minJ]![k]! - dij) / 2
225
- D[newIdx]![k] = Math.max(0, newDist)
226
- D[k]![newIdx] = Math.max(0, newDist)
218
+ D[minI]![k] = Math.max(0, newDist)
219
+ D[k]![minI] = Math.max(0, newDist)
227
220
  }
228
221
  }
229
222
 
230
223
  // Mark minJ as removed
231
224
  nodes[minJ] = undefined
232
- nodes[newIdx] = newNode
225
+ nodes[minI] = newNode
233
226
 
234
227
  remaining--
235
228
  }
@@ -257,25 +250,45 @@ function neighborJoining(distances: number[][], names: string[]): NJNode {
257
250
  return nodes[finalActive[0]!]!
258
251
  }
259
252
 
260
- function nodeToNewick(node: NJNode, branchLength?: number): string {
261
- let result: string
262
-
263
- if (node.name !== undefined && !node.left && !node.right) {
264
- const quotedName = node.name.replaceAll("'", "''")
265
- result = `'${quotedName}'`
266
- } else {
267
- const leftNewick = node.left ? nodeToNewick(node.left, node.leftLength) : ''
268
- const rightNewick = node.right
269
- ? nodeToNewick(node.right, node.rightLength)
270
- : ''
271
- result = `(${leftNewick},${rightNewick})`
272
- }
253
+ function withBranchLength(newick: string, branchLength?: number) {
254
+ return branchLength === undefined
255
+ ? newick
256
+ : `${newick}:${branchLength.toFixed(6)}`
257
+ }
273
258
 
274
- if (branchLength !== undefined) {
275
- result += `:${branchLength.toFixed(6)}`
259
+ // Emit Newick iteratively rather than recursively: an NJ tree of N leaves can
260
+ // be a caterpillar of depth ~N, so recursion risks a stack overflow on large
261
+ // alignments (matching the iterative-traversal convention in hierarchy.ts).
262
+ function nodeToNewick(root: NJNode): string {
263
+ const postOrder: NJNode[] = []
264
+ const stack: NJNode[] = [root]
265
+ while (stack.length > 0) {
266
+ const node = stack.pop()!
267
+ postOrder.push(node)
268
+ if (node.left) {
269
+ stack.push(node.left)
270
+ }
271
+ if (node.right) {
272
+ stack.push(node.right)
273
+ }
276
274
  }
277
275
 
278
- return result
276
+ const newickByNode = new Map<NJNode, string>()
277
+ for (let i = postOrder.length - 1; i >= 0; i--) {
278
+ const node = postOrder[i]!
279
+ if (node.name !== undefined && !node.left && !node.right) {
280
+ newickByNode.set(node, `'${node.name.replaceAll("'", "''")}'`)
281
+ } else {
282
+ const left = node.left
283
+ ? withBranchLength(newickByNode.get(node.left)!, node.leftLength)
284
+ : ''
285
+ const right = node.right
286
+ ? withBranchLength(newickByNode.get(node.right)!, node.rightLength)
287
+ : ''
288
+ newickByNode.set(node, `(${left},${right})`)
289
+ }
290
+ }
291
+ return newickByNode.get(root)!
279
292
  }
280
293
 
281
294
  export function calculateNeighborJoiningTree(
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '5.3.0'
1
+ export const version = '5.4.1'