react-msaview 5.1.1 → 5.2.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.
- 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/calculateBlocks.d.ts +4 -10
- package/dist/calculateBlocks.js +12 -20
- package/dist/calculateBlocks.js.map +1 -1
- package/dist/components/ConservationTrack.js +20 -37
- 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 +25 -34
- 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/ExportSVGDialog.js +20 -20
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/dialogs/InterProScanDialog.js +6 -1
- package/dist/components/dialogs/InterProScanDialog.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 +3 -2
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/Header.js +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/header/settingsMenuItems.js +24 -8
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/minimap/Minimap.js +6 -17
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/minimap/MinimapSVG.js +7 -17
- 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/getVisibleLeaves.d.ts +6 -0
- package/dist/components/msa/getVisibleLeaves.js +9 -0
- package/dist/components/msa/getVisibleLeaves.js.map +1 -0
- package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +2 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +5 -6
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.d.ts +2 -1
- package/dist/components/msa/renderMSABlock.js +100 -88
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +5 -4
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/msa/visibleColRange.d.ts +8 -0
- package/dist/components/msa/visibleColRange.js +10 -0
- package/dist/components/msa/visibleColRange.js.map +1 -0
- package/dist/components/renderCtx.d.ts +7 -0
- package/dist/components/renderCtx.js +2 -0
- package/dist/components/renderCtx.js.map +1 -0
- package/dist/components/tracks/renderTracksSvg.d.ts +6 -5
- package/dist/components/tracks/renderTracksSvg.js +13 -5
- package/dist/components/tracks/renderTracksSvg.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +2 -10
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +5 -4
- package/dist/components/tree/renderTreeCanvas.js +18 -26
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/util.d.ts +0 -14
- package/dist/components/util.js +0 -58
- package/dist/components/util.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 +3 -1
- package/dist/hierarchy.js +142 -89
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -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 +234 -176
- package/dist/model.js +285 -259
- package/dist/model.js.map +1 -1
- package/dist/parseAsn1.js +8 -2
- package/dist/parseAsn1.js.map +1 -1
- package/dist/renderToSvg.d.ts +1 -1
- package/dist/renderToSvg.js +43 -46
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rowCoordinateCalculations.d.ts +0 -9
- package/dist/rowCoordinateCalculations.js +0 -20
- package/dist/rowCoordinateCalculations.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 +5 -0
- package/dist/util.js +59 -1
- 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/calculateBlocks.ts +18 -51
- package/src/components/ConservationTrack.tsx +20 -45
- package/src/components/MSAView.tsx +10 -1
- package/src/components/ResizeHandles.tsx +101 -113
- package/src/components/TextTrack.tsx +31 -38
- package/src/components/Track.tsx +1 -1
- package/src/components/VerticalScrollbar.tsx +2 -2
- package/src/components/dialogs/ExportSVGDialog.tsx +27 -17
- package/src/components/dialogs/InterProScanDialog.tsx +6 -1
- package/src/components/dialogs/UserProvidedDomainsDialog.tsx +1 -2
- package/src/components/header/ColorSchemeMenu.tsx +1 -0
- package/src/components/header/FileMenu.tsx +3 -1
- package/src/components/header/Header.tsx +1 -1
- 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/header/settingsMenuItems.ts +24 -8
- package/src/components/minimap/Minimap.tsx +6 -20
- package/src/components/minimap/MinimapSVG.tsx +10 -27
- 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/getVisibleLeaves.ts +21 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +9 -9
- package/src/components/msa/renderMSABlock.ts +123 -125
- package/src/components/msa/renderMSAMouseover.ts +5 -4
- package/src/components/msa/visibleColRange.ts +17 -0
- package/src/components/renderCtx.ts +29 -0
- package/src/components/tracks/renderTracksSvg.ts +20 -10
- package/src/components/tree/TreeCanvas.tsx +3 -1
- package/src/components/tree/TreeCanvasBlock.tsx +2 -12
- package/src/components/tree/renderTreeCanvas.ts +27 -30
- package/src/components/util.ts +0 -81
- 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 +153 -100
- package/src/index.ts +8 -0
- package/src/launchInterProScan.ts +33 -11
- package/src/measureTextCanvas.ts +3 -1
- package/src/model.ts +359 -306
- package/src/parseAsn1.test.ts +6 -0
- package/src/parseAsn1.ts +11 -3
- package/src/renderToSvg.tsx +99 -95
- package/src/rowCoordinateCalculations.test.ts +0 -51
- package/src/rowCoordinateCalculations.ts +0 -23
- 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 +60 -1
- 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
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
parseNewick,
|
|
20
20
|
} from 'msa-parsers'
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { calculateBlocks } from './calculateBlocks.ts'
|
|
23
23
|
import { clustalXColumnColors } from './clustalX.ts'
|
|
24
24
|
import colorSchemes from './colorSchemes.ts'
|
|
25
25
|
import ConservationTrack from './components/ConservationTrack.tsx'
|
|
@@ -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
|
|
@@ -317,13 +336,6 @@ function stateModelFactory() {
|
|
|
317
336
|
*/
|
|
318
337
|
highlightedColumns: undefined as number[] | undefined,
|
|
319
338
|
|
|
320
|
-
/**
|
|
321
|
-
* #volatile
|
|
322
|
-
* a dummy variable that is incremented when ref changes so autorun for
|
|
323
|
-
* drawing canvas commands will run
|
|
324
|
-
*/
|
|
325
|
-
nref: 0,
|
|
326
|
-
|
|
327
339
|
/**
|
|
328
340
|
* #volatile
|
|
329
341
|
*/
|
|
@@ -389,6 +401,14 @@ function stateModelFactory() {
|
|
|
389
401
|
setWidth(arg: number) {
|
|
390
402
|
self.volatileWidth = arg
|
|
391
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
|
+
},
|
|
392
412
|
/**
|
|
393
413
|
* #action
|
|
394
414
|
* set the height of the view in px
|
|
@@ -408,38 +428,23 @@ function stateModelFactory() {
|
|
|
408
428
|
/**
|
|
409
429
|
* #action
|
|
410
430
|
* set mouse position (row, column) in the MSA
|
|
431
|
+
*
|
|
432
|
+
* CROSS-REPO CONTRACT: jbrowse-plugin-protein3d calls this (and reads the
|
|
433
|
+
* `mouseCol` volatile) to sync MSA<->3D-structure hover. Keep the name and
|
|
434
|
+
* signature stable; see that repo's ProteinToMsaHoverSync.tsx.
|
|
411
435
|
*/
|
|
412
436
|
setMousePos(col?: number, row?: number) {
|
|
413
437
|
self.mouseCol = col
|
|
414
438
|
self.mouseRow = row
|
|
415
439
|
},
|
|
416
440
|
|
|
417
|
-
/**
|
|
418
|
-
* #action
|
|
419
|
-
* set hovered tree node and its descendants
|
|
420
|
-
*/
|
|
421
|
-
setHoveredTreeNode(nodeId?: string) {
|
|
422
|
-
if (!nodeId) {
|
|
423
|
-
self.hoveredTreeNode = undefined
|
|
424
|
-
return
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const node = find(
|
|
428
|
-
(self as MsaViewModel).hierarchy,
|
|
429
|
-
n => n.data.id === nodeId,
|
|
430
|
-
)
|
|
431
|
-
if (!node) {
|
|
432
|
-
self.hoveredTreeNode = undefined
|
|
433
|
-
return
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const descendantNames = leaves(node).map(leaf => leaf.data.name)
|
|
437
|
-
|
|
438
|
-
self.hoveredTreeNode = { nodeId, descendantNames }
|
|
439
|
-
},
|
|
440
441
|
/**
|
|
441
442
|
* #action
|
|
442
443
|
* set highlighted columns
|
|
444
|
+
*
|
|
445
|
+
* CROSS-REPO CONTRACT: called by jbrowse-plugin-msaview
|
|
446
|
+
* (afterCreateAutoruns.ts) to highlight alignment columns. It has no
|
|
447
|
+
* in-repo caller, so do not flag it as dead code — it is public API.
|
|
443
448
|
*/
|
|
444
449
|
setHighlightedColumns(columns?: number[]) {
|
|
445
450
|
self.highlightedColumns = columns
|
|
@@ -618,21 +623,26 @@ function stateModelFactory() {
|
|
|
618
623
|
* #getter
|
|
619
624
|
*/
|
|
620
625
|
get colorScheme() {
|
|
621
|
-
|
|
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
|
+
)
|
|
622
632
|
},
|
|
623
633
|
|
|
624
634
|
/**
|
|
625
635
|
* #getter
|
|
626
636
|
*/
|
|
627
637
|
get header() {
|
|
628
|
-
return
|
|
638
|
+
return this.MSA?.getHeader() ?? {}
|
|
629
639
|
},
|
|
630
640
|
|
|
631
641
|
/**
|
|
632
642
|
* #getter
|
|
633
643
|
*/
|
|
634
644
|
get alignmentNames() {
|
|
635
|
-
return this.MSA?.alignmentNames
|
|
645
|
+
return this.MSA?.alignmentNames ?? []
|
|
636
646
|
},
|
|
637
647
|
/**
|
|
638
648
|
* #getter
|
|
@@ -665,7 +675,7 @@ function stateModelFactory() {
|
|
|
665
675
|
* #getter
|
|
666
676
|
*/
|
|
667
677
|
get numColumns() {
|
|
668
|
-
return (this.MSA?.getWidth()
|
|
678
|
+
return (this.MSA?.getWidth() ?? 0) - this.blanks.length
|
|
669
679
|
},
|
|
670
680
|
|
|
671
681
|
/**
|
|
@@ -673,35 +683,40 @@ function stateModelFactory() {
|
|
|
673
683
|
*/
|
|
674
684
|
get tree(): NodeWithIds {
|
|
675
685
|
const text = self.data.tree
|
|
676
|
-
|
|
677
686
|
return text
|
|
678
|
-
? generateNodeIds(
|
|
679
|
-
|
|
680
|
-
? flatToTree(parseAsn1(text))
|
|
681
|
-
: parseNewick(
|
|
682
|
-
text.startsWith('SEQ') ? parseEmfTree(text).tree : text,
|
|
683
|
-
),
|
|
684
|
-
)
|
|
685
|
-
: this.MSA?.getTree() || {
|
|
687
|
+
? generateNodeIds(parseTreeText(text))
|
|
688
|
+
: (this.MSA?.getTree() ?? {
|
|
686
689
|
noTree: true,
|
|
687
690
|
children: [],
|
|
688
691
|
id: 'empty',
|
|
689
692
|
name: 'empty',
|
|
690
|
-
}
|
|
693
|
+
})
|
|
691
694
|
},
|
|
692
695
|
|
|
693
696
|
/**
|
|
694
697
|
* #getter
|
|
698
|
+
* Returns the list of row (sequence) names in display order.
|
|
699
|
+
* Part of the public API used by downstream consumers (e.g. jbrowse plugins).
|
|
695
700
|
*/
|
|
696
701
|
get rowNames(): string[] {
|
|
697
702
|
return this.leaves.map(n => n.data.name)
|
|
698
703
|
},
|
|
704
|
+
/**
|
|
705
|
+
* #getter
|
|
706
|
+
*/
|
|
707
|
+
get rowNamesSet() {
|
|
708
|
+
return new Map(
|
|
709
|
+
this.leaves.map((leaf, index) => [leaf.data.name, index] as const),
|
|
710
|
+
)
|
|
711
|
+
},
|
|
699
712
|
/**
|
|
700
713
|
* #getter
|
|
701
714
|
*/
|
|
702
715
|
get mouseOverRowName() {
|
|
703
716
|
const { mouseRow } = self
|
|
704
|
-
return mouseRow === undefined
|
|
717
|
+
return mouseRow === undefined
|
|
718
|
+
? undefined
|
|
719
|
+
: this.leaves[mouseRow]?.data.name
|
|
705
720
|
},
|
|
706
721
|
/**
|
|
707
722
|
* #getter
|
|
@@ -709,23 +724,15 @@ function stateModelFactory() {
|
|
|
709
724
|
*/
|
|
710
725
|
get hoveredInsertion() {
|
|
711
726
|
const { mouseCol, mouseRow } = self
|
|
712
|
-
if (mouseCol
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
return undefined
|
|
722
|
-
}
|
|
723
|
-
const insertion = insertions.find(ins => ins.pos === mouseCol)
|
|
724
|
-
if (insertion) {
|
|
725
|
-
return {
|
|
726
|
-
rowName,
|
|
727
|
-
col: mouseCol,
|
|
728
|
-
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
|
+
}
|
|
729
736
|
}
|
|
730
737
|
}
|
|
731
738
|
return undefined
|
|
@@ -748,15 +755,14 @@ function stateModelFactory() {
|
|
|
748
755
|
|
|
749
756
|
for (const collapsedId of self.collapsed) {
|
|
750
757
|
const node = find(hier, n => n.data.id === collapsedId)
|
|
751
|
-
if (
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
)
|
|
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
|
+
}
|
|
760
766
|
}
|
|
761
767
|
}
|
|
762
768
|
|
|
@@ -768,7 +774,7 @@ function stateModelFactory() {
|
|
|
768
774
|
* widget width minus the tree area gives the space for the MSA
|
|
769
775
|
*/
|
|
770
776
|
get msaAreaWidth() {
|
|
771
|
-
return self.width - self.treeAreaWidth
|
|
777
|
+
return self.width - self.treeAreaWidth - self.resizeHandleWidth
|
|
772
778
|
},
|
|
773
779
|
|
|
774
780
|
/**
|
|
@@ -823,49 +829,13 @@ function stateModelFactory() {
|
|
|
823
829
|
* Returns a map of row name to array of insertions with display position and letters
|
|
824
830
|
*/
|
|
825
831
|
get insertionPositions() {
|
|
826
|
-
const { hideGapsEffective } = self
|
|
827
832
|
const { blanks, rows } = this
|
|
828
|
-
|
|
829
|
-
if (blanksLen === 0 || !hideGapsEffective) {
|
|
833
|
+
if (blanks.length === 0 || !self.hideGapsEffective) {
|
|
830
834
|
return new Map<string, { pos: number; letters: string }[]>()
|
|
831
835
|
}
|
|
832
836
|
const result = new Map<string, { pos: number; letters: string }[]>()
|
|
833
837
|
for (const [name, seq] of rows) {
|
|
834
|
-
const insertions
|
|
835
|
-
let displayPos = 0
|
|
836
|
-
let blankIdx = 0
|
|
837
|
-
let currentInsertPos = -1
|
|
838
|
-
let letterChars: string[] = []
|
|
839
|
-
const seqLen = seq.length
|
|
840
|
-
for (let i = 0; i < seqLen; i++) {
|
|
841
|
-
if (blankIdx < blanksLen && blanks[blankIdx] === i) {
|
|
842
|
-
// bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
|
|
843
|
-
const code = seq.charCodeAt(i)
|
|
844
|
-
if (!((code - 45) >>> 0 <= 1)) {
|
|
845
|
-
if (currentInsertPos === displayPos) {
|
|
846
|
-
letterChars.push(seq[i]!)
|
|
847
|
-
} else {
|
|
848
|
-
if (letterChars.length > 0) {
|
|
849
|
-
insertions.push({
|
|
850
|
-
pos: currentInsertPos,
|
|
851
|
-
letters: letterChars.join(''),
|
|
852
|
-
})
|
|
853
|
-
}
|
|
854
|
-
currentInsertPos = displayPos
|
|
855
|
-
letterChars = [seq[i]!]
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
blankIdx++
|
|
859
|
-
} else {
|
|
860
|
-
displayPos++
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
if (letterChars.length > 0) {
|
|
864
|
-
insertions.push({
|
|
865
|
-
pos: currentInsertPos,
|
|
866
|
-
letters: letterChars.join(''),
|
|
867
|
-
})
|
|
868
|
-
}
|
|
838
|
+
const insertions = computeRowInsertions(blanks, seq)
|
|
869
839
|
if (insertions.length > 0) {
|
|
870
840
|
result.set(name, insertions)
|
|
871
841
|
}
|
|
@@ -898,10 +868,9 @@ function stateModelFactory() {
|
|
|
898
868
|
* #getter
|
|
899
869
|
*/
|
|
900
870
|
get columns() {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
),
|
|
871
|
+
const columns2d = this.columns2d
|
|
872
|
+
return new Map(
|
|
873
|
+
this.rows.map((row, index) => [row[0], columns2d[index]!] as const),
|
|
905
874
|
)
|
|
906
875
|
},
|
|
907
876
|
/**
|
|
@@ -909,9 +878,12 @@ function stateModelFactory() {
|
|
|
909
878
|
*/
|
|
910
879
|
get columns2d() {
|
|
911
880
|
const { hideGapsEffective } = self
|
|
912
|
-
return this.rows
|
|
913
|
-
|
|
914
|
-
|
|
881
|
+
return this.rows.map(([, str]) =>
|
|
882
|
+
(hideGapsEffective
|
|
883
|
+
? skipBlanks(this.blanks, str)
|
|
884
|
+
: str
|
|
885
|
+
).toUpperCase(),
|
|
886
|
+
)
|
|
915
887
|
},
|
|
916
888
|
/**
|
|
917
889
|
* #getter
|
|
@@ -923,7 +895,7 @@ function stateModelFactory() {
|
|
|
923
895
|
* #getter
|
|
924
896
|
*/
|
|
925
897
|
get colStats() {
|
|
926
|
-
const r
|
|
898
|
+
const r: Record<string, number>[] = []
|
|
927
899
|
const columns = this.columns2d
|
|
928
900
|
for (const column of columns) {
|
|
929
901
|
for (let j = 0; j < column.length; j++) {
|
|
@@ -939,7 +911,7 @@ function stateModelFactory() {
|
|
|
939
911
|
* #getter
|
|
940
912
|
*/
|
|
941
913
|
get colStatsSums() {
|
|
942
|
-
return this.colStats.map(
|
|
914
|
+
return this.colStats.map(col => sum(Object.values(col)))
|
|
943
915
|
},
|
|
944
916
|
|
|
945
917
|
/**
|
|
@@ -951,23 +923,18 @@ function stateModelFactory() {
|
|
|
951
923
|
const letters = new Set<string>()
|
|
952
924
|
for (const stats of this.colStats) {
|
|
953
925
|
for (const letter of Object.keys(stats)) {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
letters.add(upper)
|
|
926
|
+
if (letter !== '-' && letter !== '.') {
|
|
927
|
+
letters.add(letter)
|
|
957
928
|
}
|
|
958
929
|
}
|
|
959
930
|
}
|
|
931
|
+
// isDna already excludes U (not in the DNA set) and isRna excludes T,
|
|
932
|
+
// so the set membership alone disambiguates the two
|
|
960
933
|
const dna = new Set(['A', 'C', 'G', 'T', 'N'])
|
|
961
934
|
const rna = new Set(['A', 'C', 'G', 'U', 'N'])
|
|
962
|
-
const isDna = [...letters].every(l => dna.has(l))
|
|
963
|
-
const isRna = [...letters].every(l => rna.has(l))
|
|
964
|
-
|
|
965
|
-
return 'dna'
|
|
966
|
-
}
|
|
967
|
-
if (isRna && !letters.has('T')) {
|
|
968
|
-
return 'rna'
|
|
969
|
-
}
|
|
970
|
-
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'
|
|
971
938
|
},
|
|
972
939
|
|
|
973
940
|
/**
|
|
@@ -981,8 +948,7 @@ function stateModelFactory() {
|
|
|
981
948
|
const total = colStatsSums[i]!
|
|
982
949
|
let maxCount = 0
|
|
983
950
|
let letter = ''
|
|
984
|
-
for (const key
|
|
985
|
-
const val = stats[key]!
|
|
951
|
+
for (const [key, val] of Object.entries(stats)) {
|
|
986
952
|
if (val > maxCount && key !== '-' && key !== '.') {
|
|
987
953
|
maxCount = val
|
|
988
954
|
letter = key
|
|
@@ -1029,25 +995,16 @@ function stateModelFactory() {
|
|
|
1029
995
|
return 0
|
|
1030
996
|
}
|
|
1031
997
|
|
|
1032
|
-
const gapCount = (stats['-']
|
|
998
|
+
const gapCount = (stats['-'] ?? 0) + (stats['.'] ?? 0)
|
|
1033
999
|
const nonGapTotal = total - gapCount
|
|
1034
1000
|
if (nonGapTotal === 0) {
|
|
1035
1001
|
return 0
|
|
1036
1002
|
}
|
|
1037
1003
|
|
|
1038
|
-
const merged: Record<string, number> = {}
|
|
1039
|
-
for (const letter of Object.keys(stats)) {
|
|
1040
|
-
if (letter === '-' || letter === '.') {
|
|
1041
|
-
continue
|
|
1042
|
-
}
|
|
1043
|
-
const upper = letter.toUpperCase()
|
|
1044
|
-
merged[upper] = (merged[upper] || 0) + stats[letter]!
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
1004
|
let entropy = 0
|
|
1048
|
-
for (const
|
|
1049
|
-
|
|
1050
|
-
|
|
1005
|
+
for (const [letter, count] of Object.entries(stats)) {
|
|
1006
|
+
if (letter !== '-' && letter !== '.') {
|
|
1007
|
+
const freq = count / nonGapTotal
|
|
1051
1008
|
entropy -= freq * Math.log2(freq)
|
|
1052
1009
|
}
|
|
1053
1010
|
}
|
|
@@ -1065,7 +1022,8 @@ function stateModelFactory() {
|
|
|
1065
1022
|
const r = this.root
|
|
1066
1023
|
clusterLayout(r, this.totalHeight, self.treeWidth)
|
|
1067
1024
|
r.data.length = 0
|
|
1068
|
-
|
|
1025
|
+
const max = maxLength(r)
|
|
1026
|
+
setBrLength(r, 0, max ? self.treeWidth / max : 0)
|
|
1069
1027
|
return r as HierarchyNode<NodeWithIdsAndLength>
|
|
1070
1028
|
},
|
|
1071
1029
|
|
|
@@ -1128,28 +1086,28 @@ function stateModelFactory() {
|
|
|
1128
1086
|
* #getter
|
|
1129
1087
|
*/
|
|
1130
1088
|
get dataInitialized() {
|
|
1131
|
-
return (self.data.msa || self.data.tree) && !self.error
|
|
1089
|
+
return (self.data.msa !== '' || self.data.tree !== '') && !self.error
|
|
1132
1090
|
},
|
|
1133
1091
|
/**
|
|
1134
1092
|
* #getter
|
|
1135
1093
|
*/
|
|
1136
1094
|
get blocksX() {
|
|
1137
|
-
return
|
|
1138
|
-
|
|
1139
|
-
|
|
1095
|
+
return calculateBlocks({
|
|
1096
|
+
viewportSize: self.msaAreaWidth,
|
|
1097
|
+
viewportPos: -self.scrollX,
|
|
1140
1098
|
blockSize: self.blockSize,
|
|
1141
|
-
|
|
1099
|
+
mapSize: self.totalWidth,
|
|
1142
1100
|
})
|
|
1143
1101
|
},
|
|
1144
1102
|
/**
|
|
1145
1103
|
* #getter
|
|
1146
1104
|
*/
|
|
1147
1105
|
get blocksY() {
|
|
1148
|
-
return
|
|
1149
|
-
|
|
1150
|
-
|
|
1106
|
+
return calculateBlocks({
|
|
1107
|
+
viewportSize: self.height,
|
|
1108
|
+
viewportPos: -self.scrollY,
|
|
1151
1109
|
blockSize: self.blockSize,
|
|
1152
|
-
|
|
1110
|
+
mapSize: self.totalHeight,
|
|
1153
1111
|
})
|
|
1154
1112
|
},
|
|
1155
1113
|
}))
|
|
@@ -1175,6 +1133,19 @@ function stateModelFactory() {
|
|
|
1175
1133
|
get maxScrollX() {
|
|
1176
1134
|
return Math.min(-self.totalWidth + (self.msaAreaWidth - 100), 0)
|
|
1177
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
|
+
},
|
|
1178
1149
|
/**
|
|
1179
1150
|
* #getter
|
|
1180
1151
|
*/
|
|
@@ -1200,6 +1171,31 @@ function stateModelFactory() {
|
|
|
1200
1171
|
self.drawMsaLetters = arg
|
|
1201
1172
|
},
|
|
1202
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
|
+
|
|
1203
1199
|
/**
|
|
1204
1200
|
* #action
|
|
1205
1201
|
* Calculate a neighbor joining tree from the current MSA using BLOSUM62 distances
|
|
@@ -1223,35 +1219,41 @@ function stateModelFactory() {
|
|
|
1223
1219
|
* #action
|
|
1224
1220
|
*/
|
|
1225
1221
|
zoomOutHorizontal() {
|
|
1226
|
-
self.colWidth = Math.max(
|
|
1222
|
+
self.colWidth = Math.max(minColWidth, Math.floor(self.colWidth * 0.75))
|
|
1227
1223
|
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
1228
1224
|
},
|
|
1229
1225
|
/**
|
|
1230
1226
|
* #action
|
|
1231
1227
|
*/
|
|
1232
1228
|
zoomInHorizontal() {
|
|
1233
|
-
self.colWidth = Math.ceil(self.colWidth * 1.5)
|
|
1229
|
+
self.colWidth = Math.min(maxCellSize, Math.ceil(self.colWidth * 1.5))
|
|
1234
1230
|
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
1235
1231
|
},
|
|
1236
1232
|
/**
|
|
1237
1233
|
* #action
|
|
1238
1234
|
*/
|
|
1239
1235
|
zoomInVertical() {
|
|
1240
|
-
self.rowHeight = Math.ceil(self.rowHeight * 1.5)
|
|
1236
|
+
self.rowHeight = Math.min(maxCellSize, Math.ceil(self.rowHeight * 1.5))
|
|
1241
1237
|
},
|
|
1242
1238
|
/**
|
|
1243
1239
|
* #action
|
|
1244
1240
|
*/
|
|
1245
1241
|
zoomOutVertical() {
|
|
1246
|
-
self.rowHeight = Math.max(
|
|
1242
|
+
self.rowHeight = Math.max(
|
|
1243
|
+
minRowHeight,
|
|
1244
|
+
Math.floor(self.rowHeight * 0.75),
|
|
1245
|
+
)
|
|
1247
1246
|
},
|
|
1248
1247
|
/**
|
|
1249
1248
|
* #action
|
|
1250
1249
|
*/
|
|
1251
1250
|
zoomIn() {
|
|
1252
1251
|
transaction(() => {
|
|
1253
|
-
self.colWidth = Math.ceil(self.colWidth * 1.5)
|
|
1254
|
-
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
|
+
)
|
|
1255
1257
|
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
1256
1258
|
})
|
|
1257
1259
|
},
|
|
@@ -1260,27 +1262,79 @@ function stateModelFactory() {
|
|
|
1260
1262
|
*/
|
|
1261
1263
|
zoomOut() {
|
|
1262
1264
|
transaction(() => {
|
|
1263
|
-
self.colWidth = Math.max(
|
|
1264
|
-
|
|
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
|
+
)
|
|
1265
1273
|
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
1266
1274
|
})
|
|
1267
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
|
+
},
|
|
1268
1317
|
/**
|
|
1269
1318
|
* #action
|
|
1270
1319
|
*/
|
|
1271
1320
|
doScrollY(deltaY: number) {
|
|
1272
|
-
self.scrollY = clamp(self.scrollY + deltaY,
|
|
1321
|
+
self.scrollY = clamp(self.scrollY + deltaY, self.maxScrollY, 0)
|
|
1273
1322
|
},
|
|
1274
1323
|
|
|
1275
|
-
|
|
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>) {
|
|
1276
1331
|
self.interProAnnotations = data
|
|
1332
|
+
self.setShowDomains(!!data)
|
|
1277
1333
|
},
|
|
1278
1334
|
|
|
1279
1335
|
applyGFFText(gffText: string) {
|
|
1280
1336
|
const gffRecords = parseGFF(gffText)
|
|
1281
|
-
|
|
1282
|
-
self.interProAnnotations = interProResults
|
|
1283
|
-
self.setShowDomains(true)
|
|
1337
|
+
this.setDomains(gffToInterProResults(gffRecords))
|
|
1284
1338
|
},
|
|
1285
1339
|
|
|
1286
1340
|
/**
|
|
@@ -1318,22 +1372,22 @@ function stateModelFactory() {
|
|
|
1318
1372
|
/**
|
|
1319
1373
|
* #getter
|
|
1320
1374
|
*/
|
|
1321
|
-
get
|
|
1375
|
+
get labelWidthMap() {
|
|
1322
1376
|
const { rowHeight, leaves, treeMetadata, fontSize } = self
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
)
|
|
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
|
+
)
|
|
1386
|
+
},
|
|
1387
|
+
|
|
1388
|
+
get labelsWidth() {
|
|
1389
|
+
const widths = this.labelWidthMap
|
|
1390
|
+
return widths.size === 0 ? 0 : Math.max(...widths.values())
|
|
1337
1391
|
},
|
|
1338
1392
|
|
|
1339
1393
|
/**
|
|
@@ -1355,18 +1409,13 @@ function stateModelFactory() {
|
|
|
1355
1409
|
*/
|
|
1356
1410
|
get adapterTrackModels(): BasicTrack[] {
|
|
1357
1411
|
const { rowHeight, MSA, hideGapsEffective, blanks } = self
|
|
1358
|
-
const tracks =
|
|
1359
|
-
data?: string
|
|
1360
|
-
})[]
|
|
1412
|
+
const tracks = MSA?.tracks ?? []
|
|
1361
1413
|
return tracks
|
|
1362
1414
|
.filter(t => !!t.data)
|
|
1363
1415
|
.map(t => ({
|
|
1364
1416
|
model: {
|
|
1365
1417
|
...t,
|
|
1366
|
-
data:
|
|
1367
|
-
hideGapsEffective && t.data
|
|
1368
|
-
? skipBlanks(blanks, t.data)
|
|
1369
|
-
: t.data,
|
|
1418
|
+
data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data,
|
|
1370
1419
|
height: rowHeight,
|
|
1371
1420
|
},
|
|
1372
1421
|
ReactComponent: TextTrack,
|
|
@@ -1402,13 +1451,6 @@ function stateModelFactory() {
|
|
|
1402
1451
|
return self.msaAreaWidth < self.totalWidth
|
|
1403
1452
|
},
|
|
1404
1453
|
|
|
1405
|
-
/**
|
|
1406
|
-
* #getter
|
|
1407
|
-
*/
|
|
1408
|
-
get rowNamesSet() {
|
|
1409
|
-
return new Map(self.rowNames.map((r, idx) => [r, idx]))
|
|
1410
|
-
},
|
|
1411
|
-
|
|
1412
1454
|
/**
|
|
1413
1455
|
* #method
|
|
1414
1456
|
* Return a row-specific letter at a visible column, or undefined if gap.
|
|
@@ -1427,6 +1469,11 @@ function stateModelFactory() {
|
|
|
1427
1469
|
* Convert a visible column to a row-specific sequence position (0-based).
|
|
1428
1470
|
* Returns undefined if the position is a gap in the sequence.
|
|
1429
1471
|
*
|
|
1472
|
+
* CROSS-REPO CONTRACT: this and the sibling coordinate converters
|
|
1473
|
+
* (seqPosToVisibleCol, globalColToVisibleCol, seqPosToGlobalCol) are used
|
|
1474
|
+
* by jbrowse-plugin-protein3d to translate between alignment columns and
|
|
1475
|
+
* structure/sequence residue positions across gaps. Keep them stable.
|
|
1476
|
+
*
|
|
1430
1477
|
* @param rowName - The name of the row
|
|
1431
1478
|
* @param visibleCol - The visible column index
|
|
1432
1479
|
* @returns The sequence position (0-based), or undefined if it's a gap
|
|
@@ -1480,14 +1527,8 @@ function stateModelFactory() {
|
|
|
1480
1527
|
* @returns The global column index in the full MSA
|
|
1481
1528
|
*/
|
|
1482
1529
|
seqPosToGlobalCol(rowName: string, seqPos: number) {
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
return index !== undefined && rows[index]
|
|
1486
|
-
? seqPosToGlobalCol({
|
|
1487
|
-
row: rows[index][1],
|
|
1488
|
-
seqPos,
|
|
1489
|
-
})
|
|
1490
|
-
: 0
|
|
1530
|
+
const seq = self.rowMap.get(rowName)
|
|
1531
|
+
return seq ? seqPosToGlobalCol({ row: seq, seqPos }) : 0
|
|
1491
1532
|
},
|
|
1492
1533
|
|
|
1493
1534
|
/**
|
|
@@ -1506,30 +1547,6 @@ function stateModelFactory() {
|
|
|
1506
1547
|
}))
|
|
1507
1548
|
|
|
1508
1549
|
.views(self => ({
|
|
1509
|
-
/**
|
|
1510
|
-
* #getter
|
|
1511
|
-
* Returns information about the currently hovered cell
|
|
1512
|
-
*/
|
|
1513
|
-
get hoveredCell() {
|
|
1514
|
-
const { mouseCol, mouseRow } = self
|
|
1515
|
-
if (mouseCol === undefined || mouseRow === undefined) {
|
|
1516
|
-
return undefined
|
|
1517
|
-
}
|
|
1518
|
-
const rowName = self.rowNames[mouseRow]
|
|
1519
|
-
if (!rowName) {
|
|
1520
|
-
return undefined
|
|
1521
|
-
}
|
|
1522
|
-
const seq = self.columns[rowName]
|
|
1523
|
-
const base = seq?.[mouseCol]
|
|
1524
|
-
const seqPos = self.visibleColToSeqPosOneBased(rowName, mouseCol)
|
|
1525
|
-
return {
|
|
1526
|
-
rowName,
|
|
1527
|
-
col: mouseCol,
|
|
1528
|
-
base,
|
|
1529
|
-
seqPos,
|
|
1530
|
-
}
|
|
1531
|
-
},
|
|
1532
|
-
|
|
1533
1550
|
/**
|
|
1534
1551
|
* #getter
|
|
1535
1552
|
* widget width minus the tree area gives the space for the MSA
|
|
@@ -1554,26 +1571,25 @@ function stateModelFactory() {
|
|
|
1554
1571
|
)
|
|
1555
1572
|
},
|
|
1556
1573
|
get tidyInterProAnnotations() {
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
.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
|
+
: []
|
|
1577
1593
|
},
|
|
1578
1594
|
get tidyFilteredInterProAnnotations() {
|
|
1579
1595
|
return this.tidyInterProAnnotations.filter(r =>
|
|
@@ -1603,12 +1619,10 @@ function stateModelFactory() {
|
|
|
1603
1619
|
return createPaletteMap([...self.tidyInterProAnnotationTypes.keys()])
|
|
1604
1620
|
},
|
|
1605
1621
|
get strokePalette() {
|
|
1606
|
-
return
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
]),
|
|
1611
|
-
)
|
|
1622
|
+
return transform(this.fillPalette, ([key, val]) => [
|
|
1623
|
+
key,
|
|
1624
|
+
colord(val).darken(0.1).toHex(),
|
|
1625
|
+
])
|
|
1612
1626
|
},
|
|
1613
1627
|
|
|
1614
1628
|
/**
|
|
@@ -1650,8 +1664,7 @@ function stateModelFactory() {
|
|
|
1650
1664
|
self.setTreeFilehandle(undefined)
|
|
1651
1665
|
self.setMSAFilehandle(undefined)
|
|
1652
1666
|
self.setGFFFilehandle(undefined)
|
|
1653
|
-
self.
|
|
1654
|
-
self.setShowDomains(false)
|
|
1667
|
+
self.setDomains(undefined)
|
|
1655
1668
|
},
|
|
1656
1669
|
/**
|
|
1657
1670
|
* #action
|
|
@@ -1660,21 +1673,13 @@ function stateModelFactory() {
|
|
|
1660
1673
|
theme: Theme
|
|
1661
1674
|
includeMinimap?: boolean
|
|
1662
1675
|
includeTracks?: boolean
|
|
1663
|
-
exportType:
|
|
1676
|
+
exportType: 'entire' | 'viewport'
|
|
1664
1677
|
}) {
|
|
1665
1678
|
const { renderToSvg } = await import('./renderToSvg.tsx')
|
|
1666
1679
|
const html = await renderToSvg(self as MsaViewModel, opts)
|
|
1667
1680
|
const blob = new Blob([html], { type: 'image/svg+xml' })
|
|
1668
1681
|
saveAs(blob, 'image.svg')
|
|
1669
1682
|
},
|
|
1670
|
-
/**
|
|
1671
|
-
* #action
|
|
1672
|
-
* internal, used for drawing to canvas
|
|
1673
|
-
*/
|
|
1674
|
-
incrementRef() {
|
|
1675
|
-
self.nref++
|
|
1676
|
-
},
|
|
1677
|
-
|
|
1678
1683
|
initFilter(arg: string) {
|
|
1679
1684
|
if (!self.featureFilters.has(arg)) {
|
|
1680
1685
|
self.featureFilters.set(arg, true)
|
|
@@ -1688,8 +1693,20 @@ function stateModelFactory() {
|
|
|
1688
1693
|
* #action
|
|
1689
1694
|
*/
|
|
1690
1695
|
fit() {
|
|
1691
|
-
self.
|
|
1692
|
-
|
|
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
|
+
}
|
|
1693
1710
|
self.scrollX = 0
|
|
1694
1711
|
self.scrollY = 0
|
|
1695
1712
|
},
|
|
@@ -1697,14 +1714,26 @@ function stateModelFactory() {
|
|
|
1697
1714
|
* #action
|
|
1698
1715
|
*/
|
|
1699
1716
|
fitVertically() {
|
|
1700
|
-
self.
|
|
1717
|
+
if (self.numRows > 0) {
|
|
1718
|
+
self.rowHeight = clamp(
|
|
1719
|
+
self.msaAreaHeight / self.numRows,
|
|
1720
|
+
minRowHeight,
|
|
1721
|
+
maxCellSize,
|
|
1722
|
+
)
|
|
1723
|
+
}
|
|
1701
1724
|
self.scrollY = 0
|
|
1702
1725
|
},
|
|
1703
1726
|
/**
|
|
1704
1727
|
* #action
|
|
1705
1728
|
*/
|
|
1706
1729
|
fitHorizontally() {
|
|
1707
|
-
self.
|
|
1730
|
+
if (self.numColumns > 0) {
|
|
1731
|
+
self.colWidth = clamp(
|
|
1732
|
+
self.msaAreaWidth / self.numColumns,
|
|
1733
|
+
minColWidth,
|
|
1734
|
+
maxCellSize,
|
|
1735
|
+
)
|
|
1736
|
+
}
|
|
1708
1737
|
self.scrollX = 0
|
|
1709
1738
|
},
|
|
1710
1739
|
|
|
@@ -1718,26 +1747,39 @@ function stateModelFactory() {
|
|
|
1718
1747
|
}),
|
|
1719
1748
|
)
|
|
1720
1749
|
|
|
1721
|
-
// 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
|
|
1722
1754
|
addDisposer(
|
|
1723
1755
|
self,
|
|
1724
1756
|
autorun(async () => {
|
|
1725
1757
|
const { treeFilehandle } = self
|
|
1726
1758
|
if (treeFilehandle) {
|
|
1759
|
+
const generation = ++treeGeneration
|
|
1727
1760
|
try {
|
|
1728
1761
|
self.setLoadingTree(true)
|
|
1729
|
-
|
|
1730
|
-
|
|
1762
|
+
const text = await fetchAndMaybeUnzipText(
|
|
1763
|
+
openLocation(treeFilehandle),
|
|
1731
1764
|
)
|
|
1732
|
-
if (
|
|
1733
|
-
|
|
1734
|
-
|
|
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
|
+
})
|
|
1735
1773
|
}
|
|
1736
1774
|
} catch (e) {
|
|
1737
|
-
|
|
1738
|
-
|
|
1775
|
+
if (generation === treeGeneration) {
|
|
1776
|
+
console.error(e)
|
|
1777
|
+
self.setError(e)
|
|
1778
|
+
}
|
|
1739
1779
|
} finally {
|
|
1740
|
-
|
|
1780
|
+
if (generation === treeGeneration) {
|
|
1781
|
+
self.setLoadingTree(false)
|
|
1782
|
+
}
|
|
1741
1783
|
}
|
|
1742
1784
|
}
|
|
1743
1785
|
}),
|
|
@@ -1800,36 +1842,47 @@ function stateModelFactory() {
|
|
|
1800
1842
|
}),
|
|
1801
1843
|
)
|
|
1802
1844
|
|
|
1803
|
-
// autorun opens msaFilehandle
|
|
1845
|
+
// autorun opens msaFilehandle. generation guard: see treeFilehandle
|
|
1846
|
+
let msaGeneration = 0
|
|
1804
1847
|
addDisposer(
|
|
1805
1848
|
self,
|
|
1806
1849
|
autorun(async () => {
|
|
1807
1850
|
const { msaFilehandle } = self
|
|
1808
1851
|
if (msaFilehandle) {
|
|
1852
|
+
const generation = ++msaGeneration
|
|
1809
1853
|
try {
|
|
1810
1854
|
self.setLoadingMSA(true)
|
|
1811
1855
|
self.setError(undefined)
|
|
1812
1856
|
const txt = await fetchAndMaybeUnzipText(
|
|
1813
1857
|
openLocation(msaFilehandle),
|
|
1814
1858
|
)
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
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
|
+
}
|
|
1822
1868
|
} catch (e) {
|
|
1823
|
-
|
|
1824
|
-
|
|
1869
|
+
if (generation === msaGeneration) {
|
|
1870
|
+
console.error(e)
|
|
1871
|
+
self.setError(e)
|
|
1872
|
+
}
|
|
1825
1873
|
} finally {
|
|
1826
|
-
|
|
1874
|
+
if (generation === msaGeneration) {
|
|
1875
|
+
self.setLoadingMSA(false)
|
|
1876
|
+
}
|
|
1827
1877
|
}
|
|
1828
1878
|
}
|
|
1829
1879
|
}),
|
|
1830
1880
|
)
|
|
1831
1881
|
|
|
1832
|
-
//
|
|
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.
|
|
1833
1886
|
// xref solution https://github.com/mobxjs/mobx/issues/266#issuecomment-222007278
|
|
1834
1887
|
// xref problem https://github.com/GMOD/react-msaview/issues/75
|
|
1835
1888
|
addDisposer(
|