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/dist/model.js
CHANGED
|
@@ -5,12 +5,12 @@ import { addDisposer, cast, types } from '@jbrowse/mobx-state-tree';
|
|
|
5
5
|
import { colord } from 'colord';
|
|
6
6
|
import { autorun, transaction } from 'mobx';
|
|
7
7
|
import { generateNodeIds, gffToInterProResults, parseEmfTree, parseGFF, parseMSA, parseNewick, } from 'msa-parsers';
|
|
8
|
-
import {
|
|
8
|
+
import { calculateBlocks } from "./calculateBlocks.js";
|
|
9
9
|
import { clustalXColumnColors } from "./clustalX.js";
|
|
10
10
|
import colorSchemes from "./colorSchemes.js";
|
|
11
11
|
import ConservationTrack from "./components/ConservationTrack.js";
|
|
12
12
|
import TextTrack from "./components/TextTrack.js";
|
|
13
|
-
import { defaultAllowedGappyness, defaultBgColor, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawLabels, defaultDrawMsaLetters, defaultDrawNodeBubbles, defaultDrawTree, defaultHeight, defaultHideGaps, defaultLabelsAlignRight, defaultRowHeight, defaultScrollX, defaultScrollY, defaultShowBranchLen, defaultShowDomains, defaultSubFeatureRows, defaultTreeAreaWidth, defaultTreeWidth, } from "./constants.js";
|
|
13
|
+
import { defaultAllowedGappyness, defaultBgColor, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawLabels, defaultDrawMsaLetters, defaultDrawNodeBubbles, defaultDrawTree, defaultHeight, defaultHideGaps, defaultLabelsAlignRight, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultShowBranchLen, defaultShowDomains, defaultSubFeatureRows, defaultTreeAreaWidth, defaultTreeWidth, maxCellSize, minColWidth, minRowHeight, } from "./constants.js";
|
|
14
14
|
import { createPaletteMap } from "./createPaletteMap.js";
|
|
15
15
|
import { flatToTree } from "./flatToTree.js";
|
|
16
16
|
import { calcDepthToLeaf, clusterLayout, collapse, find, findMaxBranchLen, hierarchy, leaves, links, maxLength, setBrLength, sort, sum as hierarchySum, } from "./hierarchy.js";
|
|
@@ -23,14 +23,25 @@ import { calculateNeighborJoiningTree } from "./neighborJoining.js";
|
|
|
23
23
|
import { parseAsn1 } from "./parseAsn1.js";
|
|
24
24
|
import { globalColToVisibleCol, visibleColToGlobalCol, visibleColToSeqPosForRow, } from "./rowCoordinateCalculations.js";
|
|
25
25
|
import { seqPosToGlobalCol } from "./seqPosToGlobalCol.js";
|
|
26
|
-
import { len, skipBlanks } from "./util.js";
|
|
26
|
+
import { computeRowInsertions, len, skipBlanks, transform } from "./util.js";
|
|
27
27
|
import { saveAs } from "./vendor/fileSaver.js";
|
|
28
|
+
function parseTreeText(text) {
|
|
29
|
+
if (text.startsWith('BioTreeContainer')) {
|
|
30
|
+
return flatToTree(parseAsn1(text));
|
|
31
|
+
}
|
|
32
|
+
return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text);
|
|
33
|
+
}
|
|
28
34
|
/**
|
|
29
35
|
* #stateModel MsaView
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* -
|
|
33
|
-
* -
|
|
36
|
+
* #example
|
|
37
|
+
* ```js
|
|
38
|
+
* import { MSAModelF } from 'react-msaview'
|
|
39
|
+
* import { types } from '@jbrowse/mobx-state-tree'
|
|
40
|
+
*
|
|
41
|
+
* const RootModel = types.model({ view: types.optional(MSAModelF(), {}) })
|
|
42
|
+
* const root = RootModel.create({})
|
|
43
|
+
* root.view.setData({ msa: '>seq1\nACGT\n>seq2\nACGT' })
|
|
44
|
+
* ```
|
|
34
45
|
*/
|
|
35
46
|
function stateModelFactory() {
|
|
36
47
|
return types
|
|
@@ -65,6 +76,11 @@ function stateModelFactory() {
|
|
|
65
76
|
* #property
|
|
66
77
|
*/
|
|
67
78
|
drawMsaLetters: defaultDrawMsaLetters,
|
|
79
|
+
/**
|
|
80
|
+
* #property
|
|
81
|
+
* zoom in/out on plain mouse-wheel without holding ctrl
|
|
82
|
+
*/
|
|
83
|
+
scrollZoom: defaultScrollZoom,
|
|
68
84
|
/**
|
|
69
85
|
* #property
|
|
70
86
|
* height of the div containing the view, px
|
|
@@ -162,9 +178,10 @@ function stateModelFactory() {
|
|
|
162
178
|
/**
|
|
163
179
|
* #volatile
|
|
164
180
|
* high resolution scale factor, helps make canvas look better on hi-dpi
|
|
165
|
-
* screens
|
|
181
|
+
* screens. derived from the device pixel ratio so canvases are crisp on
|
|
182
|
+
* retina/4k displays and not needlessly oversized on standard ones
|
|
166
183
|
*/
|
|
167
|
-
highResScaleFactor:
|
|
184
|
+
highResScaleFactor: typeof window === 'undefined' ? 1 : window.devicePixelRatio,
|
|
168
185
|
/**
|
|
169
186
|
* #volatile
|
|
170
187
|
*/
|
|
@@ -217,12 +234,6 @@ function stateModelFactory() {
|
|
|
217
234
|
* array of column indices to highlight
|
|
218
235
|
*/
|
|
219
236
|
highlightedColumns: undefined,
|
|
220
|
-
/**
|
|
221
|
-
* #volatile
|
|
222
|
-
* a dummy variable that is incremented when ref changes so autorun for
|
|
223
|
-
* drawing canvas commands will run
|
|
224
|
-
*/
|
|
225
|
-
nref: 0,
|
|
226
237
|
/**
|
|
227
238
|
* #volatile
|
|
228
239
|
*/
|
|
@@ -282,6 +293,14 @@ function stateModelFactory() {
|
|
|
282
293
|
setWidth(arg) {
|
|
283
294
|
self.volatileWidth = arg;
|
|
284
295
|
},
|
|
296
|
+
/**
|
|
297
|
+
* #action
|
|
298
|
+
* high-res scale factor, tracks the device pixel ratio so canvases stay
|
|
299
|
+
* crisp when the window moves between monitors or the browser zooms
|
|
300
|
+
*/
|
|
301
|
+
setHighResScaleFactor(arg) {
|
|
302
|
+
self.highResScaleFactor = arg;
|
|
303
|
+
},
|
|
285
304
|
/**
|
|
286
305
|
* #action
|
|
287
306
|
* set the height of the view in px
|
|
@@ -299,31 +318,22 @@ function stateModelFactory() {
|
|
|
299
318
|
/**
|
|
300
319
|
* #action
|
|
301
320
|
* set mouse position (row, column) in the MSA
|
|
321
|
+
*
|
|
322
|
+
* CROSS-REPO CONTRACT: jbrowse-plugin-protein3d calls this (and reads the
|
|
323
|
+
* `mouseCol` volatile) to sync MSA<->3D-structure hover. Keep the name and
|
|
324
|
+
* signature stable; see that repo's ProteinToMsaHoverSync.tsx.
|
|
302
325
|
*/
|
|
303
326
|
setMousePos(col, row) {
|
|
304
327
|
self.mouseCol = col;
|
|
305
328
|
self.mouseRow = row;
|
|
306
329
|
},
|
|
307
|
-
/**
|
|
308
|
-
* #action
|
|
309
|
-
* set hovered tree node and its descendants
|
|
310
|
-
*/
|
|
311
|
-
setHoveredTreeNode(nodeId) {
|
|
312
|
-
if (!nodeId) {
|
|
313
|
-
self.hoveredTreeNode = undefined;
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
const node = find(self.hierarchy, n => n.data.id === nodeId);
|
|
317
|
-
if (!node) {
|
|
318
|
-
self.hoveredTreeNode = undefined;
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const descendantNames = leaves(node).map(leaf => leaf.data.name);
|
|
322
|
-
self.hoveredTreeNode = { nodeId, descendantNames };
|
|
323
|
-
},
|
|
324
330
|
/**
|
|
325
331
|
* #action
|
|
326
332
|
* set highlighted columns
|
|
333
|
+
*
|
|
334
|
+
* CROSS-REPO CONTRACT: called by jbrowse-plugin-msaview
|
|
335
|
+
* (afterCreateAutoruns.ts) to highlight alignment columns. It has no
|
|
336
|
+
* in-repo caller, so do not flag it as dead code — it is public API.
|
|
327
337
|
*/
|
|
328
338
|
setHighlightedColumns(columns) {
|
|
329
339
|
self.highlightedColumns = columns;
|
|
@@ -482,19 +492,22 @@ function stateModelFactory() {
|
|
|
482
492
|
* #getter
|
|
483
493
|
*/
|
|
484
494
|
get colorScheme() {
|
|
485
|
-
|
|
495
|
+
// colorSchemeName is a free string (menus, snapshots, URL params); fall
|
|
496
|
+
// back to the default rather than returning undefined on a stale name
|
|
497
|
+
return (colorSchemes[self.colorSchemeName] ??
|
|
498
|
+
colorSchemes[defaultColorSchemeName]);
|
|
486
499
|
},
|
|
487
500
|
/**
|
|
488
501
|
* #getter
|
|
489
502
|
*/
|
|
490
503
|
get header() {
|
|
491
|
-
return
|
|
504
|
+
return this.MSA?.getHeader() ?? {};
|
|
492
505
|
},
|
|
493
506
|
/**
|
|
494
507
|
* #getter
|
|
495
508
|
*/
|
|
496
509
|
get alignmentNames() {
|
|
497
|
-
return this.MSA?.alignmentNames
|
|
510
|
+
return this.MSA?.alignmentNames ?? [];
|
|
498
511
|
},
|
|
499
512
|
/**
|
|
500
513
|
* #getter
|
|
@@ -527,7 +540,7 @@ function stateModelFactory() {
|
|
|
527
540
|
* #getter
|
|
528
541
|
*/
|
|
529
542
|
get numColumns() {
|
|
530
|
-
return (this.MSA?.getWidth()
|
|
543
|
+
return (this.MSA?.getWidth() ?? 0) - this.blanks.length;
|
|
531
544
|
},
|
|
532
545
|
/**
|
|
533
546
|
* #getter
|
|
@@ -535,28 +548,36 @@ function stateModelFactory() {
|
|
|
535
548
|
get tree() {
|
|
536
549
|
const text = self.data.tree;
|
|
537
550
|
return text
|
|
538
|
-
? generateNodeIds(text
|
|
539
|
-
|
|
540
|
-
: parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text))
|
|
541
|
-
: this.MSA?.getTree() || {
|
|
551
|
+
? generateNodeIds(parseTreeText(text))
|
|
552
|
+
: (this.MSA?.getTree() ?? {
|
|
542
553
|
noTree: true,
|
|
543
554
|
children: [],
|
|
544
555
|
id: 'empty',
|
|
545
556
|
name: 'empty',
|
|
546
|
-
};
|
|
557
|
+
});
|
|
547
558
|
},
|
|
548
559
|
/**
|
|
549
560
|
* #getter
|
|
561
|
+
* Returns the list of row (sequence) names in display order.
|
|
562
|
+
* Part of the public API used by downstream consumers (e.g. jbrowse plugins).
|
|
550
563
|
*/
|
|
551
564
|
get rowNames() {
|
|
552
565
|
return this.leaves.map(n => n.data.name);
|
|
553
566
|
},
|
|
567
|
+
/**
|
|
568
|
+
* #getter
|
|
569
|
+
*/
|
|
570
|
+
get rowNamesSet() {
|
|
571
|
+
return new Map(this.leaves.map((leaf, index) => [leaf.data.name, index]));
|
|
572
|
+
},
|
|
554
573
|
/**
|
|
555
574
|
* #getter
|
|
556
575
|
*/
|
|
557
576
|
get mouseOverRowName() {
|
|
558
577
|
const { mouseRow } = self;
|
|
559
|
-
return mouseRow === undefined
|
|
578
|
+
return mouseRow === undefined
|
|
579
|
+
? undefined
|
|
580
|
+
: this.leaves[mouseRow]?.data.name;
|
|
560
581
|
},
|
|
561
582
|
/**
|
|
562
583
|
* #getter
|
|
@@ -564,24 +585,16 @@ function stateModelFactory() {
|
|
|
564
585
|
*/
|
|
565
586
|
get hoveredInsertion() {
|
|
566
587
|
const { mouseCol, mouseRow } = self;
|
|
567
|
-
if (mouseCol
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
const insertion = insertions.find(ins => ins.pos === mouseCol);
|
|
579
|
-
if (insertion) {
|
|
580
|
-
return {
|
|
581
|
-
rowName,
|
|
582
|
-
col: mouseCol,
|
|
583
|
-
letters: insertion.letters,
|
|
584
|
-
};
|
|
588
|
+
if (mouseCol !== undefined && mouseRow !== undefined) {
|
|
589
|
+
const rowName = this.leaves[mouseRow]?.data.name;
|
|
590
|
+
if (rowName) {
|
|
591
|
+
const insertion = this.insertionPositions
|
|
592
|
+
.get(rowName)
|
|
593
|
+
?.find(ins => ins.pos === mouseCol);
|
|
594
|
+
if (insertion) {
|
|
595
|
+
return { rowName, col: mouseCol, letters: insertion.letters };
|
|
596
|
+
}
|
|
597
|
+
}
|
|
585
598
|
}
|
|
586
599
|
return undefined;
|
|
587
600
|
},
|
|
@@ -600,14 +613,13 @@ function stateModelFactory() {
|
|
|
600
613
|
}
|
|
601
614
|
for (const collapsedId of self.collapsed) {
|
|
602
615
|
const node = find(hier, n => n.data.id === collapsedId);
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
node.parent.children = node.parent.children.filter(c => c.data.id !== collapsedId);
|
|
616
|
+
if (node) {
|
|
617
|
+
if (node.children) {
|
|
618
|
+
collapse(node);
|
|
619
|
+
}
|
|
620
|
+
else if (node.parent?.children) {
|
|
621
|
+
node.parent.children = node.parent.children.filter(c => c.data.id !== collapsedId);
|
|
622
|
+
}
|
|
611
623
|
}
|
|
612
624
|
}
|
|
613
625
|
return hier;
|
|
@@ -617,7 +629,7 @@ function stateModelFactory() {
|
|
|
617
629
|
* widget width minus the tree area gives the space for the MSA
|
|
618
630
|
*/
|
|
619
631
|
get msaAreaWidth() {
|
|
620
|
-
return self.width - self.treeAreaWidth;
|
|
632
|
+
return self.width - self.treeAreaWidth - self.resizeHandleWidth;
|
|
621
633
|
},
|
|
622
634
|
/**
|
|
623
635
|
* #getter
|
|
@@ -671,51 +683,13 @@ function stateModelFactory() {
|
|
|
671
683
|
* Returns a map of row name to array of insertions with display position and letters
|
|
672
684
|
*/
|
|
673
685
|
get insertionPositions() {
|
|
674
|
-
const { hideGapsEffective } = self;
|
|
675
686
|
const { blanks, rows } = this;
|
|
676
|
-
|
|
677
|
-
if (blanksLen === 0 || !hideGapsEffective) {
|
|
687
|
+
if (blanks.length === 0 || !self.hideGapsEffective) {
|
|
678
688
|
return new Map();
|
|
679
689
|
}
|
|
680
690
|
const result = new Map();
|
|
681
691
|
for (const [name, seq] of rows) {
|
|
682
|
-
const insertions =
|
|
683
|
-
let displayPos = 0;
|
|
684
|
-
let blankIdx = 0;
|
|
685
|
-
let currentInsertPos = -1;
|
|
686
|
-
let letterChars = [];
|
|
687
|
-
const seqLen = seq.length;
|
|
688
|
-
for (let i = 0; i < seqLen; i++) {
|
|
689
|
-
if (blankIdx < blanksLen && blanks[blankIdx] === i) {
|
|
690
|
-
// bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
|
|
691
|
-
const code = seq.charCodeAt(i);
|
|
692
|
-
if (!((code - 45) >>> 0 <= 1)) {
|
|
693
|
-
if (currentInsertPos === displayPos) {
|
|
694
|
-
letterChars.push(seq[i]);
|
|
695
|
-
}
|
|
696
|
-
else {
|
|
697
|
-
if (letterChars.length > 0) {
|
|
698
|
-
insertions.push({
|
|
699
|
-
pos: currentInsertPos,
|
|
700
|
-
letters: letterChars.join(''),
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
currentInsertPos = displayPos;
|
|
704
|
-
letterChars = [seq[i]];
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
blankIdx++;
|
|
708
|
-
}
|
|
709
|
-
else {
|
|
710
|
-
displayPos++;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
if (letterChars.length > 0) {
|
|
714
|
-
insertions.push({
|
|
715
|
-
pos: currentInsertPos,
|
|
716
|
-
letters: letterChars.join(''),
|
|
717
|
-
});
|
|
718
|
-
}
|
|
692
|
+
const insertions = computeRowInsertions(blanks, seq);
|
|
719
693
|
if (insertions.length > 0) {
|
|
720
694
|
result.set(name, insertions);
|
|
721
695
|
}
|
|
@@ -747,16 +721,17 @@ function stateModelFactory() {
|
|
|
747
721
|
* #getter
|
|
748
722
|
*/
|
|
749
723
|
get columns() {
|
|
750
|
-
|
|
724
|
+
const columns2d = this.columns2d;
|
|
725
|
+
return new Map(this.rows.map((row, index) => [row[0], columns2d[index]]));
|
|
751
726
|
},
|
|
752
727
|
/**
|
|
753
728
|
* #getter
|
|
754
729
|
*/
|
|
755
730
|
get columns2d() {
|
|
756
731
|
const { hideGapsEffective } = self;
|
|
757
|
-
return this.rows
|
|
758
|
-
.
|
|
759
|
-
|
|
732
|
+
return this.rows.map(([, str]) => (hideGapsEffective
|
|
733
|
+
? skipBlanks(this.blanks, str)
|
|
734
|
+
: str).toUpperCase());
|
|
760
735
|
},
|
|
761
736
|
/**
|
|
762
737
|
* #getter
|
|
@@ -783,7 +758,7 @@ function stateModelFactory() {
|
|
|
783
758
|
* #getter
|
|
784
759
|
*/
|
|
785
760
|
get colStatsSums() {
|
|
786
|
-
return this.colStats.map(
|
|
761
|
+
return this.colStats.map(col => sum(Object.values(col)));
|
|
787
762
|
},
|
|
788
763
|
/**
|
|
789
764
|
* #getter
|
|
@@ -794,23 +769,18 @@ function stateModelFactory() {
|
|
|
794
769
|
const letters = new Set();
|
|
795
770
|
for (const stats of this.colStats) {
|
|
796
771
|
for (const letter of Object.keys(stats)) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
letters.add(upper);
|
|
772
|
+
if (letter !== '-' && letter !== '.') {
|
|
773
|
+
letters.add(letter);
|
|
800
774
|
}
|
|
801
775
|
}
|
|
802
776
|
}
|
|
777
|
+
// isDna already excludes U (not in the DNA set) and isRna excludes T,
|
|
778
|
+
// so the set membership alone disambiguates the two
|
|
803
779
|
const dna = new Set(['A', 'C', 'G', 'T', 'N']);
|
|
804
780
|
const rna = new Set(['A', 'C', 'G', 'U', 'N']);
|
|
805
|
-
const isDna = [...letters].every(l => dna.has(l));
|
|
806
|
-
const isRna = [...letters].every(l => rna.has(l));
|
|
807
|
-
|
|
808
|
-
return 'dna';
|
|
809
|
-
}
|
|
810
|
-
if (isRna && !letters.has('T')) {
|
|
811
|
-
return 'rna';
|
|
812
|
-
}
|
|
813
|
-
return 'amino';
|
|
781
|
+
const isDna = letters.size > 0 && [...letters].every(l => dna.has(l));
|
|
782
|
+
const isRna = letters.size > 0 && [...letters].every(l => rna.has(l));
|
|
783
|
+
return isDna ? 'dna' : isRna ? 'rna' : 'amino';
|
|
814
784
|
},
|
|
815
785
|
/**
|
|
816
786
|
* #getter
|
|
@@ -823,8 +793,7 @@ function stateModelFactory() {
|
|
|
823
793
|
const total = colStatsSums[i];
|
|
824
794
|
let maxCount = 0;
|
|
825
795
|
let letter = '';
|
|
826
|
-
for (const key
|
|
827
|
-
const val = stats[key];
|
|
796
|
+
for (const [key, val] of Object.entries(stats)) {
|
|
828
797
|
if (val > maxCount && key !== '-' && key !== '.') {
|
|
829
798
|
maxCount = val;
|
|
830
799
|
letter = key;
|
|
@@ -864,23 +833,15 @@ function stateModelFactory() {
|
|
|
864
833
|
if (!total) {
|
|
865
834
|
return 0;
|
|
866
835
|
}
|
|
867
|
-
const gapCount = (stats['-']
|
|
836
|
+
const gapCount = (stats['-'] ?? 0) + (stats['.'] ?? 0);
|
|
868
837
|
const nonGapTotal = total - gapCount;
|
|
869
838
|
if (nonGapTotal === 0) {
|
|
870
839
|
return 0;
|
|
871
840
|
}
|
|
872
|
-
const merged = {};
|
|
873
|
-
for (const letter of Object.keys(stats)) {
|
|
874
|
-
if (letter === '-' || letter === '.') {
|
|
875
|
-
continue;
|
|
876
|
-
}
|
|
877
|
-
const upper = letter.toUpperCase();
|
|
878
|
-
merged[upper] = (merged[upper] || 0) + stats[letter];
|
|
879
|
-
}
|
|
880
841
|
let entropy = 0;
|
|
881
|
-
for (const
|
|
882
|
-
|
|
883
|
-
|
|
842
|
+
for (const [letter, count] of Object.entries(stats)) {
|
|
843
|
+
if (letter !== '-' && letter !== '.') {
|
|
844
|
+
const freq = count / nonGapTotal;
|
|
884
845
|
entropy -= freq * Math.log2(freq);
|
|
885
846
|
}
|
|
886
847
|
}
|
|
@@ -897,7 +858,8 @@ function stateModelFactory() {
|
|
|
897
858
|
const r = this.root;
|
|
898
859
|
clusterLayout(r, this.totalHeight, self.treeWidth);
|
|
899
860
|
r.data.length = 0;
|
|
900
|
-
|
|
861
|
+
const max = maxLength(r);
|
|
862
|
+
setBrLength(r, 0, max ? self.treeWidth / max : 0);
|
|
901
863
|
return r;
|
|
902
864
|
},
|
|
903
865
|
/**
|
|
@@ -953,28 +915,28 @@ function stateModelFactory() {
|
|
|
953
915
|
* #getter
|
|
954
916
|
*/
|
|
955
917
|
get dataInitialized() {
|
|
956
|
-
return (self.data.msa || self.data.tree) && !self.error;
|
|
918
|
+
return (self.data.msa !== '' || self.data.tree !== '') && !self.error;
|
|
957
919
|
},
|
|
958
920
|
/**
|
|
959
921
|
* #getter
|
|
960
922
|
*/
|
|
961
923
|
get blocksX() {
|
|
962
|
-
return
|
|
963
|
-
|
|
964
|
-
|
|
924
|
+
return calculateBlocks({
|
|
925
|
+
viewportSize: self.msaAreaWidth,
|
|
926
|
+
viewportPos: -self.scrollX,
|
|
965
927
|
blockSize: self.blockSize,
|
|
966
|
-
|
|
928
|
+
mapSize: self.totalWidth,
|
|
967
929
|
});
|
|
968
930
|
},
|
|
969
931
|
/**
|
|
970
932
|
* #getter
|
|
971
933
|
*/
|
|
972
934
|
get blocksY() {
|
|
973
|
-
return
|
|
974
|
-
|
|
975
|
-
|
|
935
|
+
return calculateBlocks({
|
|
936
|
+
viewportSize: self.height,
|
|
937
|
+
viewportPos: -self.scrollY,
|
|
976
938
|
blockSize: self.blockSize,
|
|
977
|
-
|
|
939
|
+
mapSize: self.totalHeight,
|
|
978
940
|
});
|
|
979
941
|
},
|
|
980
942
|
}))
|
|
@@ -997,6 +959,18 @@ function stateModelFactory() {
|
|
|
997
959
|
get maxScrollX() {
|
|
998
960
|
return Math.min(-self.totalWidth + (self.msaAreaWidth - 100), 0);
|
|
999
961
|
},
|
|
962
|
+
/**
|
|
963
|
+
* #getter
|
|
964
|
+
* most-negative allowed scrollY, keeping the last row in view rather than
|
|
965
|
+
* letting the whole alignment scroll off the top. visible MSA height is
|
|
966
|
+
* computed inline here because msaAreaHeight is defined later in the chain.
|
|
967
|
+
*/
|
|
968
|
+
get maxScrollY() {
|
|
969
|
+
const visibleHeight = self.height -
|
|
970
|
+
self.headerHeight -
|
|
971
|
+
(self.msaAreaWidth < self.totalWidth ? self.minimapHeight : 0);
|
|
972
|
+
return Math.min(-self.totalHeight + visibleHeight, 0);
|
|
973
|
+
},
|
|
1000
974
|
/**
|
|
1001
975
|
* #getter
|
|
1002
976
|
*/
|
|
@@ -1019,6 +993,30 @@ function stateModelFactory() {
|
|
|
1019
993
|
setDrawMsaLetters(arg) {
|
|
1020
994
|
self.drawMsaLetters = arg;
|
|
1021
995
|
},
|
|
996
|
+
/**
|
|
997
|
+
* #action
|
|
998
|
+
*/
|
|
999
|
+
setScrollZoom(arg) {
|
|
1000
|
+
self.scrollZoom = arg;
|
|
1001
|
+
},
|
|
1002
|
+
/**
|
|
1003
|
+
* #action
|
|
1004
|
+
* set hovered tree node and its descendants
|
|
1005
|
+
*/
|
|
1006
|
+
setHoveredTreeNode(nodeId) {
|
|
1007
|
+
if (nodeId) {
|
|
1008
|
+
const node = find(self.hierarchy, n => n.data.id === nodeId);
|
|
1009
|
+
self.hoveredTreeNode = node
|
|
1010
|
+
? {
|
|
1011
|
+
nodeId,
|
|
1012
|
+
descendantNames: leaves(node).map(leaf => leaf.data.name),
|
|
1013
|
+
}
|
|
1014
|
+
: undefined;
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
self.hoveredTreeNode = undefined;
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1022
1020
|
/**
|
|
1023
1021
|
* #action
|
|
1024
1022
|
* Calculate a neighbor joining tree from the current MSA using BLOSUM62 distances
|
|
@@ -1041,35 +1039,35 @@ function stateModelFactory() {
|
|
|
1041
1039
|
* #action
|
|
1042
1040
|
*/
|
|
1043
1041
|
zoomOutHorizontal() {
|
|
1044
|
-
self.colWidth = Math.max(
|
|
1042
|
+
self.colWidth = Math.max(minColWidth, Math.floor(self.colWidth * 0.75));
|
|
1045
1043
|
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0);
|
|
1046
1044
|
},
|
|
1047
1045
|
/**
|
|
1048
1046
|
* #action
|
|
1049
1047
|
*/
|
|
1050
1048
|
zoomInHorizontal() {
|
|
1051
|
-
self.colWidth = Math.ceil(self.colWidth * 1.5);
|
|
1049
|
+
self.colWidth = Math.min(maxCellSize, Math.ceil(self.colWidth * 1.5));
|
|
1052
1050
|
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0);
|
|
1053
1051
|
},
|
|
1054
1052
|
/**
|
|
1055
1053
|
* #action
|
|
1056
1054
|
*/
|
|
1057
1055
|
zoomInVertical() {
|
|
1058
|
-
self.rowHeight = Math.ceil(self.rowHeight * 1.5);
|
|
1056
|
+
self.rowHeight = Math.min(maxCellSize, Math.ceil(self.rowHeight * 1.5));
|
|
1059
1057
|
},
|
|
1060
1058
|
/**
|
|
1061
1059
|
* #action
|
|
1062
1060
|
*/
|
|
1063
1061
|
zoomOutVertical() {
|
|
1064
|
-
self.rowHeight = Math.max(
|
|
1062
|
+
self.rowHeight = Math.max(minRowHeight, Math.floor(self.rowHeight * 0.75));
|
|
1065
1063
|
},
|
|
1066
1064
|
/**
|
|
1067
1065
|
* #action
|
|
1068
1066
|
*/
|
|
1069
1067
|
zoomIn() {
|
|
1070
1068
|
transaction(() => {
|
|
1071
|
-
self.colWidth = Math.ceil(self.colWidth * 1.5);
|
|
1072
|
-
self.rowHeight = Math.ceil(self.rowHeight * 1.5);
|
|
1069
|
+
self.colWidth = Math.min(maxCellSize, Math.ceil(self.colWidth * 1.5));
|
|
1070
|
+
self.rowHeight = Math.min(maxCellSize, Math.ceil(self.rowHeight * 1.5));
|
|
1073
1071
|
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0);
|
|
1074
1072
|
});
|
|
1075
1073
|
},
|
|
@@ -1078,25 +1076,53 @@ function stateModelFactory() {
|
|
|
1078
1076
|
*/
|
|
1079
1077
|
zoomOut() {
|
|
1080
1078
|
transaction(() => {
|
|
1081
|
-
self.colWidth = Math.max(
|
|
1082
|
-
self.rowHeight = Math.max(
|
|
1079
|
+
self.colWidth = Math.max(minColWidth, Math.floor(self.colWidth * 0.75));
|
|
1080
|
+
self.rowHeight = Math.max(minRowHeight, Math.floor(self.rowHeight * 0.75));
|
|
1083
1081
|
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0);
|
|
1084
1082
|
});
|
|
1085
1083
|
},
|
|
1084
|
+
/**
|
|
1085
|
+
* #action
|
|
1086
|
+
* Smoothly zoom by a continuous scaleFactor. The column under the cursor
|
|
1087
|
+
* (offsetX/offsetY, px relative to the MSA area) stays anchored
|
|
1088
|
+
* horizontally. Vertically the anchor is biased toward the top: when the
|
|
1089
|
+
* alignment nearly fits the viewport, snap to y=0 rather than pinning a
|
|
1090
|
+
* random row under the cursor, with the bias fading out as the alignment
|
|
1091
|
+
* grows taller than the viewport (where cursor-anchoring is useful).
|
|
1092
|
+
* Drives wheel/trackpad-pinch zoom.
|
|
1093
|
+
*/
|
|
1094
|
+
zoomToPos(scaleFactor, offsetX, offsetY) {
|
|
1095
|
+
transaction(() => {
|
|
1096
|
+
const colInView = (-self.scrollX + offsetX) / self.colWidth;
|
|
1097
|
+
const rowInView = (-self.scrollY + offsetY) / self.rowHeight;
|
|
1098
|
+
self.colWidth = clamp(self.colWidth * scaleFactor, minColWidth, maxCellSize);
|
|
1099
|
+
self.rowHeight = clamp(self.rowHeight * scaleFactor, minRowHeight, maxCellSize);
|
|
1100
|
+
self.scrollX = clamp(offsetX - colInView * self.colWidth, self.maxScrollX, 0);
|
|
1101
|
+
const anchoredScrollY = offsetY - rowInView * self.rowHeight;
|
|
1102
|
+
const overflow = Math.max(0, self.totalHeight - self.height);
|
|
1103
|
+
const topBias = self.height > 0 ? clamp(1 - overflow / self.height, 0, 1) : 0;
|
|
1104
|
+
self.scrollY = clamp(anchoredScrollY * (1 - topBias), self.maxScrollY, 0);
|
|
1105
|
+
});
|
|
1106
|
+
},
|
|
1086
1107
|
/**
|
|
1087
1108
|
* #action
|
|
1088
1109
|
*/
|
|
1089
1110
|
doScrollY(deltaY) {
|
|
1090
|
-
self.scrollY = clamp(self.scrollY + deltaY,
|
|
1111
|
+
self.scrollY = clamp(self.scrollY + deltaY, self.maxScrollY, 0);
|
|
1091
1112
|
},
|
|
1092
|
-
|
|
1113
|
+
/**
|
|
1114
|
+
* #action
|
|
1115
|
+
* Set domain annotations and reveal the overlay in a single step (or clear
|
|
1116
|
+
* both when passed undefined). Shared by every domain source: InterProScan,
|
|
1117
|
+
* GFF, user-provided uploads, and NCBI CDD.
|
|
1118
|
+
*/
|
|
1119
|
+
setDomains(data) {
|
|
1093
1120
|
self.interProAnnotations = data;
|
|
1121
|
+
self.setShowDomains(!!data);
|
|
1094
1122
|
},
|
|
1095
1123
|
applyGFFText(gffText) {
|
|
1096
1124
|
const gffRecords = parseGFF(gffText);
|
|
1097
|
-
|
|
1098
|
-
self.interProAnnotations = interProResults;
|
|
1099
|
-
self.setShowDomains(true);
|
|
1125
|
+
this.setDomains(gffToInterProResults(gffRecords));
|
|
1100
1126
|
},
|
|
1101
1127
|
/**
|
|
1102
1128
|
* #action
|
|
@@ -1132,12 +1158,19 @@ function stateModelFactory() {
|
|
|
1132
1158
|
/**
|
|
1133
1159
|
* #getter
|
|
1134
1160
|
*/
|
|
1135
|
-
get
|
|
1161
|
+
get labelWidthMap() {
|
|
1136
1162
|
const { rowHeight, leaves, treeMetadata, fontSize } = self;
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1163
|
+
return rowHeight <= 5
|
|
1164
|
+
? new Map()
|
|
1165
|
+
: new Map(leaves.map(node => {
|
|
1166
|
+
const { name } = node.data;
|
|
1167
|
+
const displayName = treeMetadata[name]?.genome ?? name;
|
|
1168
|
+
return [name, measureTextCanvas(displayName, fontSize)];
|
|
1169
|
+
}));
|
|
1170
|
+
},
|
|
1171
|
+
get labelsWidth() {
|
|
1172
|
+
const widths = this.labelWidthMap;
|
|
1173
|
+
return widths.size === 0 ? 0 : Math.max(...widths.values());
|
|
1141
1174
|
},
|
|
1142
1175
|
/**
|
|
1143
1176
|
* #getter
|
|
@@ -1156,15 +1189,13 @@ function stateModelFactory() {
|
|
|
1156
1189
|
*/
|
|
1157
1190
|
get adapterTrackModels() {
|
|
1158
1191
|
const { rowHeight, MSA, hideGapsEffective, blanks } = self;
|
|
1159
|
-
const tracks =
|
|
1192
|
+
const tracks = MSA?.tracks ?? [];
|
|
1160
1193
|
return tracks
|
|
1161
1194
|
.filter(t => !!t.data)
|
|
1162
1195
|
.map(t => ({
|
|
1163
1196
|
model: {
|
|
1164
1197
|
...t,
|
|
1165
|
-
data: hideGapsEffective
|
|
1166
|
-
? skipBlanks(blanks, t.data)
|
|
1167
|
-
: t.data,
|
|
1198
|
+
data: hideGapsEffective ? skipBlanks(blanks, t.data) : t.data,
|
|
1168
1199
|
height: rowHeight,
|
|
1169
1200
|
},
|
|
1170
1201
|
ReactComponent: TextTrack,
|
|
@@ -1196,12 +1227,6 @@ function stateModelFactory() {
|
|
|
1196
1227
|
get showHorizontalScrollbar() {
|
|
1197
1228
|
return self.msaAreaWidth < self.totalWidth;
|
|
1198
1229
|
},
|
|
1199
|
-
/**
|
|
1200
|
-
* #getter
|
|
1201
|
-
*/
|
|
1202
|
-
get rowNamesSet() {
|
|
1203
|
-
return new Map(self.rowNames.map((r, idx) => [r, idx]));
|
|
1204
|
-
},
|
|
1205
1230
|
/**
|
|
1206
1231
|
* #method
|
|
1207
1232
|
* Return a row-specific letter at a visible column, or undefined if gap.
|
|
@@ -1219,6 +1244,11 @@ function stateModelFactory() {
|
|
|
1219
1244
|
* Convert a visible column to a row-specific sequence position (0-based).
|
|
1220
1245
|
* Returns undefined if the position is a gap in the sequence.
|
|
1221
1246
|
*
|
|
1247
|
+
* CROSS-REPO CONTRACT: this and the sibling coordinate converters
|
|
1248
|
+
* (seqPosToVisibleCol, globalColToVisibleCol, seqPosToGlobalCol) are used
|
|
1249
|
+
* by jbrowse-plugin-protein3d to translate between alignment columns and
|
|
1250
|
+
* structure/sequence residue positions across gaps. Keep them stable.
|
|
1251
|
+
*
|
|
1222
1252
|
* @param rowName - The name of the row
|
|
1223
1253
|
* @param visibleCol - The visible column index
|
|
1224
1254
|
* @returns The sequence position (0-based), or undefined if it's a gap
|
|
@@ -1269,14 +1299,8 @@ function stateModelFactory() {
|
|
|
1269
1299
|
* @returns The global column index in the full MSA
|
|
1270
1300
|
*/
|
|
1271
1301
|
seqPosToGlobalCol(rowName, seqPos) {
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
return index !== undefined && rows[index]
|
|
1275
|
-
? seqPosToGlobalCol({
|
|
1276
|
-
row: rows[index][1],
|
|
1277
|
-
seqPos,
|
|
1278
|
-
})
|
|
1279
|
-
: 0;
|
|
1302
|
+
const seq = self.rowMap.get(rowName);
|
|
1303
|
+
return seq ? seqPosToGlobalCol({ row: seq, seqPos }) : 0;
|
|
1280
1304
|
},
|
|
1281
1305
|
/**
|
|
1282
1306
|
* #method
|
|
@@ -1293,29 +1317,6 @@ function stateModelFactory() {
|
|
|
1293
1317
|
},
|
|
1294
1318
|
}))
|
|
1295
1319
|
.views(self => ({
|
|
1296
|
-
/**
|
|
1297
|
-
* #getter
|
|
1298
|
-
* Returns information about the currently hovered cell
|
|
1299
|
-
*/
|
|
1300
|
-
get hoveredCell() {
|
|
1301
|
-
const { mouseCol, mouseRow } = self;
|
|
1302
|
-
if (mouseCol === undefined || mouseRow === undefined) {
|
|
1303
|
-
return undefined;
|
|
1304
|
-
}
|
|
1305
|
-
const rowName = self.rowNames[mouseRow];
|
|
1306
|
-
if (!rowName) {
|
|
1307
|
-
return undefined;
|
|
1308
|
-
}
|
|
1309
|
-
const seq = self.columns[rowName];
|
|
1310
|
-
const base = seq?.[mouseCol];
|
|
1311
|
-
const seqPos = self.visibleColToSeqPosOneBased(rowName, mouseCol);
|
|
1312
|
-
return {
|
|
1313
|
-
rowName,
|
|
1314
|
-
col: mouseCol,
|
|
1315
|
-
base,
|
|
1316
|
-
seqPos,
|
|
1317
|
-
};
|
|
1318
|
-
},
|
|
1319
1320
|
/**
|
|
1320
1321
|
* #getter
|
|
1321
1322
|
* widget width minus the tree area gives the space for the MSA
|
|
@@ -1336,22 +1337,23 @@ function stateModelFactory() {
|
|
|
1336
1337
|
return new Map(this.tidyInterProAnnotations.map(annot => [annot.accession, annot]));
|
|
1337
1338
|
},
|
|
1338
1339
|
get tidyInterProAnnotations() {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1340
|
+
return self.interProAnnotations
|
|
1341
|
+
? Object.entries(self.interProAnnotations)
|
|
1342
|
+
.flatMap(([id, val]) => val.matches.flatMap(({ signature, locations }) => {
|
|
1343
|
+
const { entry } = signature;
|
|
1344
|
+
return entry
|
|
1345
|
+
? locations.map(({ start, end }) => ({
|
|
1346
|
+
id,
|
|
1347
|
+
name: entry.name,
|
|
1348
|
+
accession: entry.accession,
|
|
1349
|
+
description: entry.description,
|
|
1350
|
+
start,
|
|
1351
|
+
end,
|
|
1352
|
+
}))
|
|
1353
|
+
: [];
|
|
1352
1354
|
}))
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
+
.toSorted((a, b) => len(b) - len(a))
|
|
1356
|
+
: [];
|
|
1355
1357
|
},
|
|
1356
1358
|
get tidyFilteredInterProAnnotations() {
|
|
1357
1359
|
return this.tidyInterProAnnotations.filter(r => self.featureFilters.get(r.accession));
|
|
@@ -1379,10 +1381,10 @@ function stateModelFactory() {
|
|
|
1379
1381
|
return createPaletteMap([...self.tidyInterProAnnotationTypes.keys()]);
|
|
1380
1382
|
},
|
|
1381
1383
|
get strokePalette() {
|
|
1382
|
-
return
|
|
1384
|
+
return transform(this.fillPalette, ([key, val]) => [
|
|
1383
1385
|
key,
|
|
1384
1386
|
colord(val).darken(0.1).toHex(),
|
|
1385
|
-
])
|
|
1387
|
+
]);
|
|
1386
1388
|
},
|
|
1387
1389
|
/**
|
|
1388
1390
|
* #method
|
|
@@ -1423,8 +1425,7 @@ function stateModelFactory() {
|
|
|
1423
1425
|
self.setTreeFilehandle(undefined);
|
|
1424
1426
|
self.setMSAFilehandle(undefined);
|
|
1425
1427
|
self.setGFFFilehandle(undefined);
|
|
1426
|
-
self.
|
|
1427
|
-
self.setShowDomains(false);
|
|
1428
|
+
self.setDomains(undefined);
|
|
1428
1429
|
},
|
|
1429
1430
|
/**
|
|
1430
1431
|
* #action
|
|
@@ -1435,13 +1436,6 @@ function stateModelFactory() {
|
|
|
1435
1436
|
const blob = new Blob([html], { type: 'image/svg+xml' });
|
|
1436
1437
|
saveAs(blob, 'image.svg');
|
|
1437
1438
|
},
|
|
1438
|
-
/**
|
|
1439
|
-
* #action
|
|
1440
|
-
* internal, used for drawing to canvas
|
|
1441
|
-
*/
|
|
1442
|
-
incrementRef() {
|
|
1443
|
-
self.nref++;
|
|
1444
|
-
},
|
|
1445
1439
|
initFilter(arg) {
|
|
1446
1440
|
if (!self.featureFilters.has(arg)) {
|
|
1447
1441
|
self.featureFilters.set(arg, true);
|
|
@@ -1454,8 +1448,12 @@ function stateModelFactory() {
|
|
|
1454
1448
|
* #action
|
|
1455
1449
|
*/
|
|
1456
1450
|
fit() {
|
|
1457
|
-
self.
|
|
1458
|
-
|
|
1451
|
+
if (self.numRows > 0) {
|
|
1452
|
+
self.rowHeight = clamp(self.msaAreaHeight / self.numRows, minRowHeight, maxCellSize);
|
|
1453
|
+
}
|
|
1454
|
+
if (self.numColumns > 0) {
|
|
1455
|
+
self.colWidth = clamp(self.msaAreaWidth / self.numColumns, minColWidth, maxCellSize);
|
|
1456
|
+
}
|
|
1459
1457
|
self.scrollX = 0;
|
|
1460
1458
|
self.scrollY = 0;
|
|
1461
1459
|
},
|
|
@@ -1463,14 +1461,18 @@ function stateModelFactory() {
|
|
|
1463
1461
|
* #action
|
|
1464
1462
|
*/
|
|
1465
1463
|
fitVertically() {
|
|
1466
|
-
self.
|
|
1464
|
+
if (self.numRows > 0) {
|
|
1465
|
+
self.rowHeight = clamp(self.msaAreaHeight / self.numRows, minRowHeight, maxCellSize);
|
|
1466
|
+
}
|
|
1467
1467
|
self.scrollY = 0;
|
|
1468
1468
|
},
|
|
1469
1469
|
/**
|
|
1470
1470
|
* #action
|
|
1471
1471
|
*/
|
|
1472
1472
|
fitHorizontally() {
|
|
1473
|
-
self.
|
|
1473
|
+
if (self.numColumns > 0) {
|
|
1474
|
+
self.colWidth = clamp(self.msaAreaWidth / self.numColumns, minColWidth, maxCellSize);
|
|
1475
|
+
}
|
|
1474
1476
|
self.scrollX = 0;
|
|
1475
1477
|
},
|
|
1476
1478
|
afterCreate() {
|
|
@@ -1479,24 +1481,37 @@ function stateModelFactory() {
|
|
|
1479
1481
|
this.initFilter(key);
|
|
1480
1482
|
}
|
|
1481
1483
|
}));
|
|
1482
|
-
// autorun opens treeFilehandle
|
|
1484
|
+
// autorun opens treeFilehandle. generation guard: if the filehandle
|
|
1485
|
+
// changes mid-fetch, a slower earlier request must not clobber the data
|
|
1486
|
+
// (or loading flag) set by the newer one
|
|
1487
|
+
let treeGeneration = 0;
|
|
1483
1488
|
addDisposer(self, autorun(async () => {
|
|
1484
1489
|
const { treeFilehandle } = self;
|
|
1485
1490
|
if (treeFilehandle) {
|
|
1491
|
+
const generation = ++treeGeneration;
|
|
1486
1492
|
try {
|
|
1487
1493
|
self.setLoadingTree(true);
|
|
1488
|
-
|
|
1489
|
-
if (
|
|
1490
|
-
|
|
1491
|
-
|
|
1494
|
+
const text = await fetchAndMaybeUnzipText(openLocation(treeFilehandle));
|
|
1495
|
+
if (generation === treeGeneration) {
|
|
1496
|
+
transaction(() => {
|
|
1497
|
+
self.setTree(text);
|
|
1498
|
+
if (treeFilehandle.locationType === 'BlobLocation') {
|
|
1499
|
+
// clear filehandle after loading if from a local file
|
|
1500
|
+
self.setTreeFilehandle(undefined);
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1492
1503
|
}
|
|
1493
1504
|
}
|
|
1494
1505
|
catch (e) {
|
|
1495
|
-
|
|
1496
|
-
|
|
1506
|
+
if (generation === treeGeneration) {
|
|
1507
|
+
console.error(e);
|
|
1508
|
+
self.setError(e);
|
|
1509
|
+
}
|
|
1497
1510
|
}
|
|
1498
1511
|
finally {
|
|
1499
|
-
|
|
1512
|
+
if (generation === treeGeneration) {
|
|
1513
|
+
self.setLoadingTree(false);
|
|
1514
|
+
}
|
|
1500
1515
|
}
|
|
1501
1516
|
}
|
|
1502
1517
|
}));
|
|
@@ -1543,32 +1558,43 @@ function stateModelFactory() {
|
|
|
1543
1558
|
}
|
|
1544
1559
|
}
|
|
1545
1560
|
}));
|
|
1546
|
-
// autorun opens msaFilehandle
|
|
1561
|
+
// autorun opens msaFilehandle. generation guard: see treeFilehandle
|
|
1562
|
+
let msaGeneration = 0;
|
|
1547
1563
|
addDisposer(self, autorun(async () => {
|
|
1548
1564
|
const { msaFilehandle } = self;
|
|
1549
1565
|
if (msaFilehandle) {
|
|
1566
|
+
const generation = ++msaGeneration;
|
|
1550
1567
|
try {
|
|
1551
1568
|
self.setLoadingMSA(true);
|
|
1552
1569
|
self.setError(undefined);
|
|
1553
1570
|
const txt = await fetchAndMaybeUnzipText(openLocation(msaFilehandle));
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1571
|
+
if (generation === msaGeneration) {
|
|
1572
|
+
transaction(() => {
|
|
1573
|
+
self.setMSA(txt);
|
|
1574
|
+
if (msaFilehandle.locationType === 'BlobLocation') {
|
|
1575
|
+
// clear filehandle after loading if from a local file
|
|
1576
|
+
self.setMSAFilehandle(undefined);
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1561
1580
|
}
|
|
1562
1581
|
catch (e) {
|
|
1563
|
-
|
|
1564
|
-
|
|
1582
|
+
if (generation === msaGeneration) {
|
|
1583
|
+
console.error(e);
|
|
1584
|
+
self.setError(e);
|
|
1585
|
+
}
|
|
1565
1586
|
}
|
|
1566
1587
|
finally {
|
|
1567
|
-
|
|
1588
|
+
if (generation === msaGeneration) {
|
|
1589
|
+
self.setLoadingMSA(false);
|
|
1590
|
+
}
|
|
1568
1591
|
}
|
|
1569
1592
|
}
|
|
1570
1593
|
}));
|
|
1571
|
-
//
|
|
1594
|
+
// Keep the parse chain warm: reading self.columns transitively holds
|
|
1595
|
+
// self.MSA (parseMSA) computed alive, so it is parsed once per data
|
|
1596
|
+
// change rather than re-parsed on every non-reactive access. colStats is
|
|
1597
|
+
// additionally held for dynamic color schemes. Do not remove.
|
|
1572
1598
|
// xref solution https://github.com/mobxjs/mobx/issues/266#issuecomment-222007278
|
|
1573
1599
|
// xref problem https://github.com/GMOD/react-msaview/issues/75
|
|
1574
1600
|
addDisposer(self, autorun(() => {
|