react-msaview 5.2.0 → 5.3.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.
- package/README.md +41 -8
- package/bundle/index.js +17 -17
- package/bundle/index.js.LICENSE.txt +1 -1
- package/bundle/index.js.map +1 -1
- package/dist/components/ConservationTrack.js +2 -13
- package/dist/components/ConservationTrack.js.map +1 -1
- package/dist/components/MSAView.js +8 -1
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/ResizeHandles.d.ts +3 -0
- package/dist/components/ResizeHandles.js +60 -105
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/TextTrack.js +2 -1
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/Track.js +1 -1
- package/dist/components/Track.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +2 -2
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/UserProvidedDomainsDialog.js +1 -2
- package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +1 -1
- package/dist/components/header/ColorSchemeMenu.js +1 -1
- package/dist/components/header/ColorSchemeMenu.js.map +1 -1
- package/dist/components/header/FileMenu.js +1 -1
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/HeaderInfoArea.js +28 -8
- package/dist/components/header/HeaderInfoArea.js.map +1 -1
- package/dist/components/header/MSASettingsMenu.js +1 -1
- package/dist/components/header/MSASettingsMenu.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.js +1 -1
- package/dist/components/header/TreeSettingsMenu.js.map +1 -1
- package/dist/components/header/ZoomControls.js +7 -2
- package/dist/components/header/ZoomControls.js.map +1 -1
- package/dist/components/header/ZoomMenu.js +9 -0
- package/dist/components/header/ZoomMenu.js.map +1 -1
- package/dist/components/minimap/Minimap.js +6 -12
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/minimap/MinimapSVG.js +7 -13
- package/dist/components/minimap/MinimapSVG.js.map +1 -1
- package/dist/components/minimap/minimapLayout.d.ts +9 -0
- package/dist/components/minimap/minimapLayout.js +15 -0
- package/dist/components/minimap/minimapLayout.js.map +1 -0
- package/dist/components/msa/MSACanvas.js +6 -1
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +5 -1
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +2 -2
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +5 -7
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +3 -2
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/tracks/renderTracksSvg.js +2 -1
- package/dist/components/tracks/renderTracksSvg.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +2 -2
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -1
- package/dist/flatToTree.js +6 -0
- package/dist/flatToTree.js.map +1 -1
- package/dist/hierarchy.d.ts +1 -1
- package/dist/hierarchy.js +136 -102
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/launchInterProScan.js +22 -9
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/measureTextCanvas.js +2 -1
- package/dist/measureTextCanvas.js.map +1 -1
- package/dist/model.d.ts +212 -150
- package/dist/model.js +239 -205
- package/dist/model.js.map +1 -1
- package/dist/parseAsn1.js +8 -2
- package/dist/parseAsn1.js.map +1 -1
- package/dist/renderToSvg.js +27 -24
- package/dist/renderToSvg.js.map +1 -1
- package/dist/setFontSize.d.ts +3 -0
- package/dist/setFontSize.js +14 -0
- package/dist/setFontSize.js.map +1 -0
- package/dist/useDevicePixelRatio.d.ts +1 -0
- package/dist/useDevicePixelRatio.js +27 -0
- package/dist/useDevicePixelRatio.js.map +1 -0
- package/dist/useWheelScroll.d.ts +3 -1
- package/dist/useWheelScroll.js +64 -22
- package/dist/useWheelScroll.js.map +1 -1
- package/dist/util.d.ts +4 -0
- package/dist/util.js +36 -0
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webpack.js +0 -1
- package/dist/webpack.js.map +1 -1
- package/dist/wheelZoom.d.ts +4 -0
- package/dist/wheelZoom.js +43 -0
- package/dist/wheelZoom.js.map +1 -0
- package/package.json +5 -5
- package/src/components/ConservationTrack.tsx +2 -18
- package/src/components/MSAView.tsx +10 -1
- package/src/components/ResizeHandles.tsx +101 -113
- package/src/components/TextTrack.tsx +2 -1
- package/src/components/Track.tsx +1 -1
- package/src/components/VerticalScrollbar.tsx +2 -2
- package/src/components/dialogs/UserProvidedDomainsDialog.tsx +1 -2
- package/src/components/header/ColorSchemeMenu.tsx +1 -0
- package/src/components/header/FileMenu.tsx +1 -0
- package/src/components/header/HeaderInfoArea.tsx +32 -7
- package/src/components/header/MSASettingsMenu.tsx +1 -0
- package/src/components/header/TreeSettingsMenu.tsx +1 -0
- package/src/components/header/ZoomControls.tsx +14 -1
- package/src/components/header/ZoomMenu.tsx +10 -0
- package/src/components/minimap/Minimap.tsx +6 -15
- package/src/components/minimap/MinimapSVG.tsx +9 -22
- package/src/components/minimap/minimapLayout.ts +17 -0
- package/src/components/msa/MSACanvas.tsx +9 -0
- package/src/components/msa/MSACanvasBlock.tsx +7 -1
- package/src/components/msa/MSAMouseoverCanvas.tsx +4 -3
- package/src/components/msa/renderMSABlock.ts +6 -9
- package/src/components/msa/renderMSAMouseover.ts +3 -1
- package/src/components/tracks/renderTracksSvg.ts +2 -1
- package/src/components/tree/renderTreeCanvas.ts +2 -2
- package/src/constants.ts +8 -0
- package/src/flatToTree.test.ts +30 -0
- package/src/flatToTree.ts +10 -1
- package/src/hierarchy.test.ts +53 -1
- package/src/hierarchy.ts +139 -113
- package/src/index.ts +1 -0
- package/src/launchInterProScan.ts +33 -11
- package/src/measureTextCanvas.ts +3 -1
- package/src/model.ts +312 -242
- package/src/parseAsn1.test.ts +6 -0
- package/src/parseAsn1.ts +11 -3
- package/src/renderToSvg.tsx +79 -67
- package/src/setFontSize.test.ts +33 -0
- package/src/setFontSize.ts +20 -0
- package/src/useDevicePixelRatio.ts +29 -0
- package/src/useWheelScroll.ts +79 -21
- package/src/util.ts +35 -0
- package/src/version.ts +1 -1
- package/src/webpack.ts +0 -1
- package/src/wheelZoom.test.ts +46 -0
- package/src/wheelZoom.ts +47 -0
package/src/model.ts
CHANGED
|
@@ -40,11 +40,15 @@ import {
|
|
|
40
40
|
defaultRowHeight,
|
|
41
41
|
defaultScrollX,
|
|
42
42
|
defaultScrollY,
|
|
43
|
+
defaultScrollZoom,
|
|
43
44
|
defaultShowBranchLen,
|
|
44
45
|
defaultShowDomains,
|
|
45
46
|
defaultSubFeatureRows,
|
|
46
47
|
defaultTreeAreaWidth,
|
|
47
48
|
defaultTreeWidth,
|
|
49
|
+
maxCellSize,
|
|
50
|
+
minColWidth,
|
|
51
|
+
minRowHeight,
|
|
48
52
|
} from './constants.ts'
|
|
49
53
|
import { createPaletteMap } from './createPaletteMap.ts'
|
|
50
54
|
import { flatToTree } from './flatToTree.ts'
|
|
@@ -75,27 +79,34 @@ import {
|
|
|
75
79
|
visibleColToSeqPosForRow,
|
|
76
80
|
} from './rowCoordinateCalculations.ts'
|
|
77
81
|
import { seqPosToGlobalCol } from './seqPosToGlobalCol.ts'
|
|
78
|
-
import { len, skipBlanks } from './util.ts'
|
|
82
|
+
import { computeRowInsertions, len, skipBlanks, transform } from './util.ts'
|
|
79
83
|
import { saveAs } from './vendor/fileSaver.ts'
|
|
80
84
|
|
|
81
85
|
import type { HierarchyNode } from './hierarchy.ts'
|
|
82
86
|
import type { InterProScanResults } from './launchInterProScan.ts'
|
|
83
|
-
import type {
|
|
84
|
-
BasicTrack,
|
|
85
|
-
NodeWithIds,
|
|
86
|
-
NodeWithIdsAndLength,
|
|
87
|
-
TextTrackModel,
|
|
88
|
-
} from './types.ts'
|
|
87
|
+
import type { BasicTrack, NodeWithIds, NodeWithIdsAndLength } from './types.ts'
|
|
89
88
|
import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
|
|
90
89
|
import type { Instance } from '@jbrowse/mobx-state-tree'
|
|
91
90
|
import type { Theme } from '@mui/material'
|
|
92
91
|
|
|
92
|
+
function parseTreeText(text: string) {
|
|
93
|
+
if (text.startsWith('BioTreeContainer')) {
|
|
94
|
+
return flatToTree(parseAsn1(text))
|
|
95
|
+
}
|
|
96
|
+
return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text)
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
/**
|
|
94
100
|
* #stateModel MsaView
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* -
|
|
98
|
-
* -
|
|
101
|
+
* #example
|
|
102
|
+
* ```js
|
|
103
|
+
* import { MSAModelF } from 'react-msaview'
|
|
104
|
+
* import { types } from '@jbrowse/mobx-state-tree'
|
|
105
|
+
*
|
|
106
|
+
* const RootModel = types.model({ view: types.optional(MSAModelF(), {}) })
|
|
107
|
+
* const root = RootModel.create({})
|
|
108
|
+
* root.view.setData({ msa: '>seq1\nACGT\n>seq2\nACGT' })
|
|
109
|
+
* ```
|
|
99
110
|
*/
|
|
100
111
|
function stateModelFactory() {
|
|
101
112
|
return types
|
|
@@ -138,6 +149,12 @@ function stateModelFactory() {
|
|
|
138
149
|
*/
|
|
139
150
|
drawMsaLetters: defaultDrawMsaLetters,
|
|
140
151
|
|
|
152
|
+
/**
|
|
153
|
+
* #property
|
|
154
|
+
* zoom in/out on plain mouse-wheel without holding ctrl
|
|
155
|
+
*/
|
|
156
|
+
scrollZoom: defaultScrollZoom,
|
|
157
|
+
|
|
141
158
|
/**
|
|
142
159
|
* #property
|
|
143
160
|
* height of the div containing the view, px
|
|
@@ -251,9 +268,11 @@ function stateModelFactory() {
|
|
|
251
268
|
/**
|
|
252
269
|
* #volatile
|
|
253
270
|
* high resolution scale factor, helps make canvas look better on hi-dpi
|
|
254
|
-
* screens
|
|
271
|
+
* screens. derived from the device pixel ratio so canvases are crisp on
|
|
272
|
+
* retina/4k displays and not needlessly oversized on standard ones
|
|
255
273
|
*/
|
|
256
|
-
highResScaleFactor:
|
|
274
|
+
highResScaleFactor:
|
|
275
|
+
typeof window === 'undefined' ? 1 : window.devicePixelRatio,
|
|
257
276
|
|
|
258
277
|
/**
|
|
259
278
|
* #volatile
|
|
@@ -382,6 +401,14 @@ function stateModelFactory() {
|
|
|
382
401
|
setWidth(arg: number) {
|
|
383
402
|
self.volatileWidth = arg
|
|
384
403
|
},
|
|
404
|
+
/**
|
|
405
|
+
* #action
|
|
406
|
+
* high-res scale factor, tracks the device pixel ratio so canvases stay
|
|
407
|
+
* crisp when the window moves between monitors or the browser zooms
|
|
408
|
+
*/
|
|
409
|
+
setHighResScaleFactor(arg: number) {
|
|
410
|
+
self.highResScaleFactor = arg
|
|
411
|
+
},
|
|
385
412
|
/**
|
|
386
413
|
* #action
|
|
387
414
|
* set the height of the view in px
|
|
@@ -411,29 +438,6 @@ function stateModelFactory() {
|
|
|
411
438
|
self.mouseRow = row
|
|
412
439
|
},
|
|
413
440
|
|
|
414
|
-
/**
|
|
415
|
-
* #action
|
|
416
|
-
* set hovered tree node and its descendants
|
|
417
|
-
*/
|
|
418
|
-
setHoveredTreeNode(nodeId?: string) {
|
|
419
|
-
if (!nodeId) {
|
|
420
|
-
self.hoveredTreeNode = undefined
|
|
421
|
-
return
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const node = find(
|
|
425
|
-
(self as MsaViewModel).hierarchy,
|
|
426
|
-
n => n.data.id === nodeId,
|
|
427
|
-
)
|
|
428
|
-
if (!node) {
|
|
429
|
-
self.hoveredTreeNode = undefined
|
|
430
|
-
return
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const descendantNames = leaves(node).map(leaf => leaf.data.name)
|
|
434
|
-
|
|
435
|
-
self.hoveredTreeNode = { nodeId, descendantNames }
|
|
436
|
-
},
|
|
437
441
|
/**
|
|
438
442
|
* #action
|
|
439
443
|
* set highlighted columns
|
|
@@ -619,14 +623,19 @@ function stateModelFactory() {
|
|
|
619
623
|
* #getter
|
|
620
624
|
*/
|
|
621
625
|
get colorScheme() {
|
|
622
|
-
|
|
626
|
+
// colorSchemeName is a free string (menus, snapshots, URL params); fall
|
|
627
|
+
// back to the default rather than returning undefined on a stale name
|
|
628
|
+
return (
|
|
629
|
+
colorSchemes[self.colorSchemeName] ??
|
|
630
|
+
colorSchemes[defaultColorSchemeName]!
|
|
631
|
+
)
|
|
623
632
|
},
|
|
624
633
|
|
|
625
634
|
/**
|
|
626
635
|
* #getter
|
|
627
636
|
*/
|
|
628
637
|
get header() {
|
|
629
|
-
return
|
|
638
|
+
return this.MSA?.getHeader() ?? {}
|
|
630
639
|
},
|
|
631
640
|
|
|
632
641
|
/**
|
|
@@ -674,26 +683,16 @@ function stateModelFactory() {
|
|
|
674
683
|
*/
|
|
675
684
|
get tree(): NodeWithIds {
|
|
676
685
|
const text = self.data.tree
|
|
677
|
-
|
|
678
686
|
return text
|
|
679
|
-
? generateNodeIds(
|
|
680
|
-
|
|
681
|
-
? flatToTree(parseAsn1(text))
|
|
682
|
-
: parseNewick(
|
|
683
|
-
text.startsWith('SEQ') ? parseEmfTree(text).tree : text,
|
|
684
|
-
),
|
|
685
|
-
)
|
|
686
|
-
: this.MSA?.getTree() || {
|
|
687
|
+
? generateNodeIds(parseTreeText(text))
|
|
688
|
+
: (this.MSA?.getTree() ?? {
|
|
687
689
|
noTree: true,
|
|
688
690
|
children: [],
|
|
689
691
|
id: 'empty',
|
|
690
692
|
name: 'empty',
|
|
691
|
-
}
|
|
693
|
+
})
|
|
692
694
|
},
|
|
693
695
|
|
|
694
|
-
/**
|
|
695
|
-
* #getter
|
|
696
|
-
*/
|
|
697
696
|
/**
|
|
698
697
|
* #getter
|
|
699
698
|
* Returns the list of row (sequence) names in display order.
|
|
@@ -706,11 +705,9 @@ function stateModelFactory() {
|
|
|
706
705
|
* #getter
|
|
707
706
|
*/
|
|
708
707
|
get rowNamesSet() {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
})
|
|
713
|
-
return map
|
|
708
|
+
return new Map(
|
|
709
|
+
this.leaves.map((leaf, index) => [leaf.data.name, index] as const),
|
|
710
|
+
)
|
|
714
711
|
},
|
|
715
712
|
/**
|
|
716
713
|
* #getter
|
|
@@ -727,23 +724,15 @@ function stateModelFactory() {
|
|
|
727
724
|
*/
|
|
728
725
|
get hoveredInsertion() {
|
|
729
726
|
const { mouseCol, mouseRow } = self
|
|
730
|
-
if (mouseCol
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
return undefined
|
|
740
|
-
}
|
|
741
|
-
const insertion = insertions.find(ins => ins.pos === mouseCol)
|
|
742
|
-
if (insertion) {
|
|
743
|
-
return {
|
|
744
|
-
rowName,
|
|
745
|
-
col: mouseCol,
|
|
746
|
-
letters: insertion.letters,
|
|
727
|
+
if (mouseCol !== undefined && mouseRow !== undefined) {
|
|
728
|
+
const rowName = this.leaves[mouseRow]?.data.name
|
|
729
|
+
if (rowName) {
|
|
730
|
+
const insertion = this.insertionPositions
|
|
731
|
+
.get(rowName)
|
|
732
|
+
?.find(ins => ins.pos === mouseCol)
|
|
733
|
+
if (insertion) {
|
|
734
|
+
return { rowName, col: mouseCol, letters: insertion.letters }
|
|
735
|
+
}
|
|
747
736
|
}
|
|
748
737
|
}
|
|
749
738
|
return undefined
|
|
@@ -766,15 +755,14 @@ function stateModelFactory() {
|
|
|
766
755
|
|
|
767
756
|
for (const collapsedId of self.collapsed) {
|
|
768
757
|
const node = find(hier, n => n.data.id === collapsedId)
|
|
769
|
-
if (
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
)
|
|
758
|
+
if (node) {
|
|
759
|
+
if (node.children) {
|
|
760
|
+
collapse(node)
|
|
761
|
+
} else if (node.parent?.children) {
|
|
762
|
+
node.parent.children = node.parent.children.filter(
|
|
763
|
+
c => c.data.id !== collapsedId,
|
|
764
|
+
)
|
|
765
|
+
}
|
|
778
766
|
}
|
|
779
767
|
}
|
|
780
768
|
|
|
@@ -786,7 +774,7 @@ function stateModelFactory() {
|
|
|
786
774
|
* widget width minus the tree area gives the space for the MSA
|
|
787
775
|
*/
|
|
788
776
|
get msaAreaWidth() {
|
|
789
|
-
return self.width - self.treeAreaWidth
|
|
777
|
+
return self.width - self.treeAreaWidth - self.resizeHandleWidth
|
|
790
778
|
},
|
|
791
779
|
|
|
792
780
|
/**
|
|
@@ -841,49 +829,13 @@ function stateModelFactory() {
|
|
|
841
829
|
* Returns a map of row name to array of insertions with display position and letters
|
|
842
830
|
*/
|
|
843
831
|
get insertionPositions() {
|
|
844
|
-
const { hideGapsEffective } = self
|
|
845
832
|
const { blanks, rows } = this
|
|
846
|
-
|
|
847
|
-
if (blanksLen === 0 || !hideGapsEffective) {
|
|
833
|
+
if (blanks.length === 0 || !self.hideGapsEffective) {
|
|
848
834
|
return new Map<string, { pos: number; letters: string }[]>()
|
|
849
835
|
}
|
|
850
836
|
const result = new Map<string, { pos: number; letters: string }[]>()
|
|
851
837
|
for (const [name, seq] of rows) {
|
|
852
|
-
const insertions
|
|
853
|
-
let displayPos = 0
|
|
854
|
-
let blankIdx = 0
|
|
855
|
-
let currentInsertPos = -1
|
|
856
|
-
let letterChars: string[] = []
|
|
857
|
-
const seqLen = seq.length
|
|
858
|
-
for (let i = 0; i < seqLen; i++) {
|
|
859
|
-
if (blankIdx < blanksLen && blanks[blankIdx] === i) {
|
|
860
|
-
// bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
|
|
861
|
-
const code = seq.charCodeAt(i)
|
|
862
|
-
if (!((code - 45) >>> 0 <= 1)) {
|
|
863
|
-
if (currentInsertPos === displayPos) {
|
|
864
|
-
letterChars.push(seq[i]!)
|
|
865
|
-
} else {
|
|
866
|
-
if (letterChars.length > 0) {
|
|
867
|
-
insertions.push({
|
|
868
|
-
pos: currentInsertPos,
|
|
869
|
-
letters: letterChars.join(''),
|
|
870
|
-
})
|
|
871
|
-
}
|
|
872
|
-
currentInsertPos = displayPos
|
|
873
|
-
letterChars = [seq[i]!]
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
blankIdx++
|
|
877
|
-
} else {
|
|
878
|
-
displayPos++
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
if (letterChars.length > 0) {
|
|
882
|
-
insertions.push({
|
|
883
|
-
pos: currentInsertPos,
|
|
884
|
-
letters: letterChars.join(''),
|
|
885
|
-
})
|
|
886
|
-
}
|
|
838
|
+
const insertions = computeRowInsertions(blanks, seq)
|
|
887
839
|
if (insertions.length > 0) {
|
|
888
840
|
result.set(name, insertions)
|
|
889
841
|
}
|
|
@@ -916,10 +868,9 @@ function stateModelFactory() {
|
|
|
916
868
|
* #getter
|
|
917
869
|
*/
|
|
918
870
|
get columns() {
|
|
871
|
+
const columns2d = this.columns2d
|
|
919
872
|
return new Map(
|
|
920
|
-
this.rows.map(
|
|
921
|
-
(row, index) => [row[0], this.columns2d[index]!] as const,
|
|
922
|
-
),
|
|
873
|
+
this.rows.map((row, index) => [row[0], columns2d[index]!] as const),
|
|
923
874
|
)
|
|
924
875
|
},
|
|
925
876
|
/**
|
|
@@ -927,14 +878,12 @@ function stateModelFactory() {
|
|
|
927
878
|
*/
|
|
928
879
|
get columns2d() {
|
|
929
880
|
const { hideGapsEffective } = self
|
|
930
|
-
return this.rows
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
).toUpperCase(),
|
|
937
|
-
)
|
|
881
|
+
return this.rows.map(([, str]) =>
|
|
882
|
+
(hideGapsEffective
|
|
883
|
+
? skipBlanks(this.blanks, str)
|
|
884
|
+
: str
|
|
885
|
+
).toUpperCase(),
|
|
886
|
+
)
|
|
938
887
|
},
|
|
939
888
|
/**
|
|
940
889
|
* #getter
|
|
@@ -946,7 +895,7 @@ function stateModelFactory() {
|
|
|
946
895
|
* #getter
|
|
947
896
|
*/
|
|
948
897
|
get colStats() {
|
|
949
|
-
const r
|
|
898
|
+
const r: Record<string, number>[] = []
|
|
950
899
|
const columns = this.columns2d
|
|
951
900
|
for (const column of columns) {
|
|
952
901
|
for (let j = 0; j < column.length; j++) {
|
|
@@ -962,13 +911,7 @@ function stateModelFactory() {
|
|
|
962
911
|
* #getter
|
|
963
912
|
*/
|
|
964
913
|
get colStatsSums() {
|
|
965
|
-
return this.colStats.map(col =>
|
|
966
|
-
let s = 0
|
|
967
|
-
for (const k in col) {
|
|
968
|
-
s += col[k]!
|
|
969
|
-
}
|
|
970
|
-
return s
|
|
971
|
-
})
|
|
914
|
+
return this.colStats.map(col => sum(Object.values(col)))
|
|
972
915
|
},
|
|
973
916
|
|
|
974
917
|
/**
|
|
@@ -985,17 +928,13 @@ function stateModelFactory() {
|
|
|
985
928
|
}
|
|
986
929
|
}
|
|
987
930
|
}
|
|
931
|
+
// isDna already excludes U (not in the DNA set) and isRna excludes T,
|
|
932
|
+
// so the set membership alone disambiguates the two
|
|
988
933
|
const dna = new Set(['A', 'C', 'G', 'T', 'N'])
|
|
989
934
|
const rna = new Set(['A', 'C', 'G', 'U', 'N'])
|
|
990
|
-
const isDna = [...letters].every(l => dna.has(l))
|
|
991
|
-
const isRna = [...letters].every(l => rna.has(l))
|
|
992
|
-
|
|
993
|
-
return 'dna'
|
|
994
|
-
}
|
|
995
|
-
if (isRna && !letters.has('T')) {
|
|
996
|
-
return 'rna'
|
|
997
|
-
}
|
|
998
|
-
return 'amino'
|
|
935
|
+
const isDna = letters.size > 0 && [...letters].every(l => dna.has(l))
|
|
936
|
+
const isRna = letters.size > 0 && [...letters].every(l => rna.has(l))
|
|
937
|
+
return isDna ? 'dna' : isRna ? 'rna' : 'amino'
|
|
999
938
|
},
|
|
1000
939
|
|
|
1001
940
|
/**
|
|
@@ -1009,8 +948,7 @@ function stateModelFactory() {
|
|
|
1009
948
|
const total = colStatsSums[i]!
|
|
1010
949
|
let maxCount = 0
|
|
1011
950
|
let letter = ''
|
|
1012
|
-
for (const key
|
|
1013
|
-
const val = stats[key]!
|
|
951
|
+
for (const [key, val] of Object.entries(stats)) {
|
|
1014
952
|
if (val > maxCount && key !== '-' && key !== '.') {
|
|
1015
953
|
maxCount = val
|
|
1016
954
|
letter = key
|
|
@@ -1064,12 +1002,9 @@ function stateModelFactory() {
|
|
|
1064
1002
|
}
|
|
1065
1003
|
|
|
1066
1004
|
let entropy = 0
|
|
1067
|
-
for (const letter of Object.
|
|
1068
|
-
if (letter
|
|
1069
|
-
|
|
1070
|
-
}
|
|
1071
|
-
const freq = stats[letter]! / nonGapTotal
|
|
1072
|
-
if (freq > 0) {
|
|
1005
|
+
for (const [letter, count] of Object.entries(stats)) {
|
|
1006
|
+
if (letter !== '-' && letter !== '.') {
|
|
1007
|
+
const freq = count / nonGapTotal
|
|
1073
1008
|
entropy -= freq * Math.log2(freq)
|
|
1074
1009
|
}
|
|
1075
1010
|
}
|
|
@@ -1087,7 +1022,8 @@ function stateModelFactory() {
|
|
|
1087
1022
|
const r = this.root
|
|
1088
1023
|
clusterLayout(r, this.totalHeight, self.treeWidth)
|
|
1089
1024
|
r.data.length = 0
|
|
1090
|
-
|
|
1025
|
+
const max = maxLength(r)
|
|
1026
|
+
setBrLength(r, 0, max ? self.treeWidth / max : 0)
|
|
1091
1027
|
return r as HierarchyNode<NodeWithIdsAndLength>
|
|
1092
1028
|
},
|
|
1093
1029
|
|
|
@@ -1150,7 +1086,7 @@ function stateModelFactory() {
|
|
|
1150
1086
|
* #getter
|
|
1151
1087
|
*/
|
|
1152
1088
|
get dataInitialized() {
|
|
1153
|
-
return (self.data.msa || self.data.tree) && !self.error
|
|
1089
|
+
return (self.data.msa !== '' || self.data.tree !== '') && !self.error
|
|
1154
1090
|
},
|
|
1155
1091
|
/**
|
|
1156
1092
|
* #getter
|
|
@@ -1197,6 +1133,19 @@ function stateModelFactory() {
|
|
|
1197
1133
|
get maxScrollX() {
|
|
1198
1134
|
return Math.min(-self.totalWidth + (self.msaAreaWidth - 100), 0)
|
|
1199
1135
|
},
|
|
1136
|
+
/**
|
|
1137
|
+
* #getter
|
|
1138
|
+
* most-negative allowed scrollY, keeping the last row in view rather than
|
|
1139
|
+
* letting the whole alignment scroll off the top. visible MSA height is
|
|
1140
|
+
* computed inline here because msaAreaHeight is defined later in the chain.
|
|
1141
|
+
*/
|
|
1142
|
+
get maxScrollY() {
|
|
1143
|
+
const visibleHeight =
|
|
1144
|
+
self.height -
|
|
1145
|
+
self.headerHeight -
|
|
1146
|
+
(self.msaAreaWidth < self.totalWidth ? self.minimapHeight : 0)
|
|
1147
|
+
return Math.min(-self.totalHeight + visibleHeight, 0)
|
|
1148
|
+
},
|
|
1200
1149
|
/**
|
|
1201
1150
|
* #getter
|
|
1202
1151
|
*/
|
|
@@ -1222,6 +1171,31 @@ function stateModelFactory() {
|
|
|
1222
1171
|
self.drawMsaLetters = arg
|
|
1223
1172
|
},
|
|
1224
1173
|
|
|
1174
|
+
/**
|
|
1175
|
+
* #action
|
|
1176
|
+
*/
|
|
1177
|
+
setScrollZoom(arg: boolean) {
|
|
1178
|
+
self.scrollZoom = arg
|
|
1179
|
+
},
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* #action
|
|
1183
|
+
* set hovered tree node and its descendants
|
|
1184
|
+
*/
|
|
1185
|
+
setHoveredTreeNode(nodeId?: string) {
|
|
1186
|
+
if (nodeId) {
|
|
1187
|
+
const node = find(self.hierarchy, n => n.data.id === nodeId)
|
|
1188
|
+
self.hoveredTreeNode = node
|
|
1189
|
+
? {
|
|
1190
|
+
nodeId,
|
|
1191
|
+
descendantNames: leaves(node).map(leaf => leaf.data.name),
|
|
1192
|
+
}
|
|
1193
|
+
: undefined
|
|
1194
|
+
} else {
|
|
1195
|
+
self.hoveredTreeNode = undefined
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
|
|
1225
1199
|
/**
|
|
1226
1200
|
* #action
|
|
1227
1201
|
* Calculate a neighbor joining tree from the current MSA using BLOSUM62 distances
|
|
@@ -1245,35 +1219,41 @@ function stateModelFactory() {
|
|
|
1245
1219
|
* #action
|
|
1246
1220
|
*/
|
|
1247
1221
|
zoomOutHorizontal() {
|
|
1248
|
-
self.colWidth = Math.max(
|
|
1222
|
+
self.colWidth = Math.max(minColWidth, Math.floor(self.colWidth * 0.75))
|
|
1249
1223
|
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
1250
1224
|
},
|
|
1251
1225
|
/**
|
|
1252
1226
|
* #action
|
|
1253
1227
|
*/
|
|
1254
1228
|
zoomInHorizontal() {
|
|
1255
|
-
self.colWidth = Math.ceil(self.colWidth * 1.5)
|
|
1229
|
+
self.colWidth = Math.min(maxCellSize, Math.ceil(self.colWidth * 1.5))
|
|
1256
1230
|
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
1257
1231
|
},
|
|
1258
1232
|
/**
|
|
1259
1233
|
* #action
|
|
1260
1234
|
*/
|
|
1261
1235
|
zoomInVertical() {
|
|
1262
|
-
self.rowHeight = Math.ceil(self.rowHeight * 1.5)
|
|
1236
|
+
self.rowHeight = Math.min(maxCellSize, Math.ceil(self.rowHeight * 1.5))
|
|
1263
1237
|
},
|
|
1264
1238
|
/**
|
|
1265
1239
|
* #action
|
|
1266
1240
|
*/
|
|
1267
1241
|
zoomOutVertical() {
|
|
1268
|
-
self.rowHeight = Math.max(
|
|
1242
|
+
self.rowHeight = Math.max(
|
|
1243
|
+
minRowHeight,
|
|
1244
|
+
Math.floor(self.rowHeight * 0.75),
|
|
1245
|
+
)
|
|
1269
1246
|
},
|
|
1270
1247
|
/**
|
|
1271
1248
|
* #action
|
|
1272
1249
|
*/
|
|
1273
1250
|
zoomIn() {
|
|
1274
1251
|
transaction(() => {
|
|
1275
|
-
self.colWidth = Math.ceil(self.colWidth * 1.5)
|
|
1276
|
-
self.rowHeight = Math.
|
|
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
|
+
)
|
|
1277
1257
|
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
1278
1258
|
})
|
|
1279
1259
|
},
|
|
@@ -1282,27 +1262,79 @@ function stateModelFactory() {
|
|
|
1282
1262
|
*/
|
|
1283
1263
|
zoomOut() {
|
|
1284
1264
|
transaction(() => {
|
|
1285
|
-
self.colWidth = Math.max(
|
|
1286
|
-
|
|
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
|
+
)
|
|
1287
1273
|
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
1288
1274
|
})
|
|
1289
1275
|
},
|
|
1276
|
+
/**
|
|
1277
|
+
* #action
|
|
1278
|
+
* Smoothly zoom by a continuous scaleFactor. The column under the cursor
|
|
1279
|
+
* (offsetX/offsetY, px relative to the MSA area) stays anchored
|
|
1280
|
+
* horizontally. Vertically the anchor is biased toward the top: when the
|
|
1281
|
+
* alignment nearly fits the viewport, snap to y=0 rather than pinning a
|
|
1282
|
+
* random row under the cursor, with the bias fading out as the alignment
|
|
1283
|
+
* grows taller than the viewport (where cursor-anchoring is useful).
|
|
1284
|
+
* Drives wheel/trackpad-pinch zoom.
|
|
1285
|
+
*/
|
|
1286
|
+
zoomToPos(scaleFactor: number, offsetX: number, offsetY: number) {
|
|
1287
|
+
transaction(() => {
|
|
1288
|
+
const colInView = (-self.scrollX + offsetX) / self.colWidth
|
|
1289
|
+
const rowInView = (-self.scrollY + offsetY) / self.rowHeight
|
|
1290
|
+
self.colWidth = clamp(
|
|
1291
|
+
self.colWidth * scaleFactor,
|
|
1292
|
+
minColWidth,
|
|
1293
|
+
maxCellSize,
|
|
1294
|
+
)
|
|
1295
|
+
self.rowHeight = clamp(
|
|
1296
|
+
self.rowHeight * scaleFactor,
|
|
1297
|
+
minRowHeight,
|
|
1298
|
+
maxCellSize,
|
|
1299
|
+
)
|
|
1300
|
+
self.scrollX = clamp(
|
|
1301
|
+
offsetX - colInView * self.colWidth,
|
|
1302
|
+
self.maxScrollX,
|
|
1303
|
+
0,
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
const anchoredScrollY = offsetY - rowInView * self.rowHeight
|
|
1307
|
+
const overflow = Math.max(0, self.totalHeight - self.height)
|
|
1308
|
+
const topBias =
|
|
1309
|
+
self.height > 0 ? clamp(1 - overflow / self.height, 0, 1) : 0
|
|
1310
|
+
self.scrollY = clamp(
|
|
1311
|
+
anchoredScrollY * (1 - topBias),
|
|
1312
|
+
self.maxScrollY,
|
|
1313
|
+
0,
|
|
1314
|
+
)
|
|
1315
|
+
})
|
|
1316
|
+
},
|
|
1290
1317
|
/**
|
|
1291
1318
|
* #action
|
|
1292
1319
|
*/
|
|
1293
1320
|
doScrollY(deltaY: number) {
|
|
1294
|
-
self.scrollY = clamp(self.scrollY + deltaY,
|
|
1321
|
+
self.scrollY = clamp(self.scrollY + deltaY, self.maxScrollY, 0)
|
|
1295
1322
|
},
|
|
1296
1323
|
|
|
1297
|
-
|
|
1324
|
+
/**
|
|
1325
|
+
* #action
|
|
1326
|
+
* Set domain annotations and reveal the overlay in a single step (or clear
|
|
1327
|
+
* both when passed undefined). Shared by every domain source: InterProScan,
|
|
1328
|
+
* GFF, user-provided uploads, and NCBI CDD.
|
|
1329
|
+
*/
|
|
1330
|
+
setDomains(data?: Record<string, InterProScanResults>) {
|
|
1298
1331
|
self.interProAnnotations = data
|
|
1332
|
+
self.setShowDomains(!!data)
|
|
1299
1333
|
},
|
|
1300
1334
|
|
|
1301
1335
|
applyGFFText(gffText: string) {
|
|
1302
1336
|
const gffRecords = parseGFF(gffText)
|
|
1303
|
-
|
|
1304
|
-
self.interProAnnotations = interProResults
|
|
1305
|
-
self.setShowDomains(true)
|
|
1337
|
+
this.setDomains(gffToInterProResults(gffRecords))
|
|
1306
1338
|
},
|
|
1307
1339
|
|
|
1308
1340
|
/**
|
|
@@ -1342,16 +1374,15 @@ function stateModelFactory() {
|
|
|
1342
1374
|
*/
|
|
1343
1375
|
get labelWidthMap() {
|
|
1344
1376
|
const { rowHeight, leaves, treeMetadata, fontSize } = self
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
)
|
|
1377
|
+
return rowHeight <= 5
|
|
1378
|
+
? new Map<string, number>()
|
|
1379
|
+
: new Map(
|
|
1380
|
+
leaves.map(node => {
|
|
1381
|
+
const { name } = node.data
|
|
1382
|
+
const displayName = treeMetadata[name]?.genome ?? name
|
|
1383
|
+
return [name, measureTextCanvas(displayName, fontSize)] as const
|
|
1384
|
+
}),
|
|
1385
|
+
)
|
|
1355
1386
|
},
|
|
1356
1387
|
|
|
1357
1388
|
get labelsWidth() {
|
|
@@ -1378,18 +1409,13 @@ function stateModelFactory() {
|
|
|
1378
1409
|
*/
|
|
1379
1410
|
get adapterTrackModels(): BasicTrack[] {
|
|
1380
1411
|
const { rowHeight, MSA, hideGapsEffective, blanks } = self
|
|
1381
|
-
const tracks =
|
|
1382
|
-
data?: string
|
|
1383
|
-
})[]
|
|
1412
|
+
const tracks = MSA?.tracks ?? []
|
|
1384
1413
|
return tracks
|
|
1385
1414
|
.filter(t => !!t.data)
|
|
1386
1415
|
.map(t => ({
|
|
1387
1416
|
model: {
|
|
1388
1417
|
...t,
|
|
1389
|
-
data:
|
|
1390
|
-
hideGapsEffective && t.data
|
|
1391
|
-
? skipBlanks(blanks, t.data)
|
|
1392
|
-
: t.data,
|
|
1418
|
+
data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data,
|
|
1393
1419
|
height: rowHeight,
|
|
1394
1420
|
},
|
|
1395
1421
|
ReactComponent: TextTrack,
|
|
@@ -1545,26 +1571,25 @@ function stateModelFactory() {
|
|
|
1545
1571
|
)
|
|
1546
1572
|
},
|
|
1547
1573
|
get tidyInterProAnnotations() {
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
.toSorted((a, b) => len(b) - len(a))
|
|
1574
|
+
return self.interProAnnotations
|
|
1575
|
+
? Object.entries(self.interProAnnotations)
|
|
1576
|
+
.flatMap(([id, val]) =>
|
|
1577
|
+
val.matches.flatMap(({ signature, locations }) => {
|
|
1578
|
+
const { entry } = signature
|
|
1579
|
+
return entry
|
|
1580
|
+
? locations.map(({ start, end }) => ({
|
|
1581
|
+
id,
|
|
1582
|
+
name: entry.name,
|
|
1583
|
+
accession: entry.accession,
|
|
1584
|
+
description: entry.description,
|
|
1585
|
+
start,
|
|
1586
|
+
end,
|
|
1587
|
+
}))
|
|
1588
|
+
: []
|
|
1589
|
+
}),
|
|
1590
|
+
)
|
|
1591
|
+
.toSorted((a, b) => len(b) - len(a))
|
|
1592
|
+
: []
|
|
1568
1593
|
},
|
|
1569
1594
|
get tidyFilteredInterProAnnotations() {
|
|
1570
1595
|
return this.tidyInterProAnnotations.filter(r =>
|
|
@@ -1594,12 +1619,10 @@ function stateModelFactory() {
|
|
|
1594
1619
|
return createPaletteMap([...self.tidyInterProAnnotationTypes.keys()])
|
|
1595
1620
|
},
|
|
1596
1621
|
get strokePalette() {
|
|
1597
|
-
return
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
]),
|
|
1602
|
-
)
|
|
1622
|
+
return transform(this.fillPalette, ([key, val]) => [
|
|
1623
|
+
key,
|
|
1624
|
+
colord(val).darken(0.1).toHex(),
|
|
1625
|
+
])
|
|
1603
1626
|
},
|
|
1604
1627
|
|
|
1605
1628
|
/**
|
|
@@ -1641,8 +1664,7 @@ function stateModelFactory() {
|
|
|
1641
1664
|
self.setTreeFilehandle(undefined)
|
|
1642
1665
|
self.setMSAFilehandle(undefined)
|
|
1643
1666
|
self.setGFFFilehandle(undefined)
|
|
1644
|
-
self.
|
|
1645
|
-
self.setShowDomains(false)
|
|
1667
|
+
self.setDomains(undefined)
|
|
1646
1668
|
},
|
|
1647
1669
|
/**
|
|
1648
1670
|
* #action
|
|
@@ -1671,8 +1693,20 @@ function stateModelFactory() {
|
|
|
1671
1693
|
* #action
|
|
1672
1694
|
*/
|
|
1673
1695
|
fit() {
|
|
1674
|
-
self.
|
|
1675
|
-
|
|
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
|
+
}
|
|
1676
1710
|
self.scrollX = 0
|
|
1677
1711
|
self.scrollY = 0
|
|
1678
1712
|
},
|
|
@@ -1680,14 +1714,26 @@ function stateModelFactory() {
|
|
|
1680
1714
|
* #action
|
|
1681
1715
|
*/
|
|
1682
1716
|
fitVertically() {
|
|
1683
|
-
self.
|
|
1717
|
+
if (self.numRows > 0) {
|
|
1718
|
+
self.rowHeight = clamp(
|
|
1719
|
+
self.msaAreaHeight / self.numRows,
|
|
1720
|
+
minRowHeight,
|
|
1721
|
+
maxCellSize,
|
|
1722
|
+
)
|
|
1723
|
+
}
|
|
1684
1724
|
self.scrollY = 0
|
|
1685
1725
|
},
|
|
1686
1726
|
/**
|
|
1687
1727
|
* #action
|
|
1688
1728
|
*/
|
|
1689
1729
|
fitHorizontally() {
|
|
1690
|
-
self.
|
|
1730
|
+
if (self.numColumns > 0) {
|
|
1731
|
+
self.colWidth = clamp(
|
|
1732
|
+
self.msaAreaWidth / self.numColumns,
|
|
1733
|
+
minColWidth,
|
|
1734
|
+
maxCellSize,
|
|
1735
|
+
)
|
|
1736
|
+
}
|
|
1691
1737
|
self.scrollX = 0
|
|
1692
1738
|
},
|
|
1693
1739
|
|
|
@@ -1701,26 +1747,39 @@ function stateModelFactory() {
|
|
|
1701
1747
|
}),
|
|
1702
1748
|
)
|
|
1703
1749
|
|
|
1704
|
-
// autorun opens treeFilehandle
|
|
1750
|
+
// autorun opens treeFilehandle. generation guard: if the filehandle
|
|
1751
|
+
// changes mid-fetch, a slower earlier request must not clobber the data
|
|
1752
|
+
// (or loading flag) set by the newer one
|
|
1753
|
+
let treeGeneration = 0
|
|
1705
1754
|
addDisposer(
|
|
1706
1755
|
self,
|
|
1707
1756
|
autorun(async () => {
|
|
1708
1757
|
const { treeFilehandle } = self
|
|
1709
1758
|
if (treeFilehandle) {
|
|
1759
|
+
const generation = ++treeGeneration
|
|
1710
1760
|
try {
|
|
1711
1761
|
self.setLoadingTree(true)
|
|
1712
|
-
|
|
1713
|
-
|
|
1762
|
+
const text = await fetchAndMaybeUnzipText(
|
|
1763
|
+
openLocation(treeFilehandle),
|
|
1714
1764
|
)
|
|
1715
|
-
if (
|
|
1716
|
-
|
|
1717
|
-
|
|
1765
|
+
if (generation === treeGeneration) {
|
|
1766
|
+
transaction(() => {
|
|
1767
|
+
self.setTree(text)
|
|
1768
|
+
if (treeFilehandle.locationType === 'BlobLocation') {
|
|
1769
|
+
// clear filehandle after loading if from a local file
|
|
1770
|
+
self.setTreeFilehandle(undefined)
|
|
1771
|
+
}
|
|
1772
|
+
})
|
|
1718
1773
|
}
|
|
1719
1774
|
} catch (e) {
|
|
1720
|
-
|
|
1721
|
-
|
|
1775
|
+
if (generation === treeGeneration) {
|
|
1776
|
+
console.error(e)
|
|
1777
|
+
self.setError(e)
|
|
1778
|
+
}
|
|
1722
1779
|
} finally {
|
|
1723
|
-
|
|
1780
|
+
if (generation === treeGeneration) {
|
|
1781
|
+
self.setLoadingTree(false)
|
|
1782
|
+
}
|
|
1724
1783
|
}
|
|
1725
1784
|
}
|
|
1726
1785
|
}),
|
|
@@ -1783,36 +1842,47 @@ function stateModelFactory() {
|
|
|
1783
1842
|
}),
|
|
1784
1843
|
)
|
|
1785
1844
|
|
|
1786
|
-
// autorun opens msaFilehandle
|
|
1845
|
+
// autorun opens msaFilehandle. generation guard: see treeFilehandle
|
|
1846
|
+
let msaGeneration = 0
|
|
1787
1847
|
addDisposer(
|
|
1788
1848
|
self,
|
|
1789
1849
|
autorun(async () => {
|
|
1790
1850
|
const { msaFilehandle } = self
|
|
1791
1851
|
if (msaFilehandle) {
|
|
1852
|
+
const generation = ++msaGeneration
|
|
1792
1853
|
try {
|
|
1793
1854
|
self.setLoadingMSA(true)
|
|
1794
1855
|
self.setError(undefined)
|
|
1795
1856
|
const txt = await fetchAndMaybeUnzipText(
|
|
1796
1857
|
openLocation(msaFilehandle),
|
|
1797
1858
|
)
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1859
|
+
if (generation === msaGeneration) {
|
|
1860
|
+
transaction(() => {
|
|
1861
|
+
self.setMSA(txt)
|
|
1862
|
+
if (msaFilehandle.locationType === 'BlobLocation') {
|
|
1863
|
+
// clear filehandle after loading if from a local file
|
|
1864
|
+
self.setMSAFilehandle(undefined)
|
|
1865
|
+
}
|
|
1866
|
+
})
|
|
1867
|
+
}
|
|
1805
1868
|
} catch (e) {
|
|
1806
|
-
|
|
1807
|
-
|
|
1869
|
+
if (generation === msaGeneration) {
|
|
1870
|
+
console.error(e)
|
|
1871
|
+
self.setError(e)
|
|
1872
|
+
}
|
|
1808
1873
|
} finally {
|
|
1809
|
-
|
|
1874
|
+
if (generation === msaGeneration) {
|
|
1875
|
+
self.setLoadingMSA(false)
|
|
1876
|
+
}
|
|
1810
1877
|
}
|
|
1811
1878
|
}
|
|
1812
1879
|
}),
|
|
1813
1880
|
)
|
|
1814
1881
|
|
|
1815
|
-
//
|
|
1882
|
+
// Keep the parse chain warm: reading self.columns transitively holds
|
|
1883
|
+
// self.MSA (parseMSA) computed alive, so it is parsed once per data
|
|
1884
|
+
// change rather than re-parsed on every non-reactive access. colStats is
|
|
1885
|
+
// additionally held for dynamic color schemes. Do not remove.
|
|
1816
1886
|
// xref solution https://github.com/mobxjs/mobx/issues/266#issuecomment-222007278
|
|
1817
1887
|
// xref problem https://github.com/GMOD/react-msaview/issues/75
|
|
1818
1888
|
addDisposer(
|