react-msaview 5.6.1 → 5.6.3
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/bundle/index.js +95 -95
- package/bundle/index.js.map +4 -4
- package/dist/clustalX.d.ts +4 -3
- package/dist/clustalX.js +25 -4
- package/dist/clustalX.js.map +1 -1
- package/dist/columnCounts.d.ts +37 -0
- package/dist/columnCounts.js +148 -0
- package/dist/columnCounts.js.map +1 -0
- package/dist/components/MSAView.js +1 -8
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/getVisibleLeaves.d.ts +6 -0
- package/dist/components/getVisibleLeaves.js.map +1 -0
- package/dist/components/msa/MSACanvasBlock.js +17 -54
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +2 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +21 -19
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +97 -96
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +9 -32
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/msa/useMsaBlockMouse.d.ts +23 -0
- package/dist/components/msa/useMsaBlockMouse.js +45 -0
- package/dist/components/msa/useMsaBlockMouse.js.map +1 -0
- package/dist/components/tree/TreeCanvas.js +14 -19
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +40 -111
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +2 -0
- package/dist/components/tree/renderTreeCanvas.js +15 -3
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/useTreeHover.d.ts +24 -0
- package/dist/components/tree/useTreeHover.js +44 -0
- package/dist/components/tree/useTreeHover.js.map +1 -0
- package/dist/hierarchy.js +15 -9
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/model/msaModel.js +4 -3
- package/dist/model/msaModel.js.map +1 -1
- package/dist/model/treeModel.js +9 -8
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +52 -10
- package/dist/model.js +208 -93
- package/dist/model.js.map +1 -1
- package/dist/propertyConservation.d.ts +2 -1
- package/dist/propertyConservation.js +36 -25
- package/dist/propertyConservation.js.map +1 -1
- package/dist/seqPosToGlobalCol.d.ts +7 -0
- package/dist/seqPosToGlobalCol.js +17 -0
- package/dist/seqPosToGlobalCol.js.map +1 -1
- package/dist/stripDefault.d.ts +21 -0
- package/dist/stripDefault.js +26 -0
- package/dist/stripDefault.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/clustalX.test.ts +9 -11
- package/src/clustalX.ts +27 -28
- package/src/columnCounts.test.ts +58 -0
- package/src/columnCounts.ts +170 -0
- package/src/components/MSAView.tsx +1 -9
- package/src/components/{msa/getVisibleLeaves.ts → getVisibleLeaves.ts} +1 -1
- package/src/components/msa/MSACanvasBlock.tsx +42 -110
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +25 -18
- package/src/components/msa/renderMSABlock.ts +113 -125
- package/src/components/msa/renderMSAMouseover.ts +11 -34
- package/src/components/msa/useMsaBlockMouse.ts +81 -0
- package/src/components/tree/TreeCanvas.tsx +16 -23
- package/src/components/tree/TreeCanvasBlock.tsx +62 -143
- package/src/components/tree/renderTreeCanvas.test.ts +100 -190
- package/src/components/tree/renderTreeCanvas.ts +14 -3
- package/src/components/tree/useTreeHover.ts +69 -0
- package/src/domainBands.test.ts +98 -0
- package/src/hierarchy.ts +15 -9
- package/src/index.ts +7 -0
- package/src/model/msaModel.ts +4 -6
- package/src/model/treeModel.ts +9 -14
- package/src/model.ts +241 -112
- package/src/propertyConservation.test.ts +2 -13
- package/src/propertyConservation.ts +40 -31
- package/src/rowSpace.test.ts +46 -0
- package/src/seqPosToGlobalCol.ts +18 -0
- package/src/stripDefault.test.ts +48 -0
- package/src/stripDefault.ts +35 -0
- package/src/types.ts +23 -0
- package/src/version.ts +1 -1
- package/dist/components/msa/getVisibleLeaves.d.ts +0 -6
- package/dist/components/msa/getVisibleLeaves.js.map +0 -1
- package/dist/useDevicePixelRatio.d.ts +0 -1
- package/dist/useDevicePixelRatio.js +0 -27
- package/dist/useDevicePixelRatio.js.map +0 -1
- package/src/useDevicePixelRatio.ts +0 -29
- /package/dist/components/{msa/getVisibleLeaves.js → getVisibleLeaves.js} +0 -0
package/dist/model.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HierarchyNode } from './hierarchy.ts';
|
|
2
2
|
import type { InterProScanResults } from './launchInterProScan.ts';
|
|
3
|
-
import type { BasicTrack, NodeWithIds, NodeWithIdsAndLength } from './types.ts';
|
|
3
|
+
import type { BasicTrack, DomainBand, NodeWithIds, NodeWithIdsAndLength } from './types.ts';
|
|
4
4
|
import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types';
|
|
5
5
|
import type { Instance } from '@jbrowse/mobx-state-tree';
|
|
6
6
|
import type { Theme } from '@mui/material';
|
|
@@ -937,8 +937,19 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
|
|
|
937
937
|
readonly rows: [string, string][];
|
|
938
938
|
/**
|
|
939
939
|
* #getter
|
|
940
|
+
* number of rows the alignment occupies on screen. This is the leaf count,
|
|
941
|
+
* not `rows.length`: a tree leaf with no matching MSA row still takes up a
|
|
942
|
+
* row of vertical space (drawn blank), so row hit-testing and fit-to-height
|
|
943
|
+
* must count it.
|
|
940
944
|
*/
|
|
941
945
|
readonly numRows: number;
|
|
946
|
+
/**
|
|
947
|
+
* #getter
|
|
948
|
+
* per-row index of the global column holding each ungapped sequence
|
|
949
|
+
* position, so seqPos -> column is a lookup rather than a scan of the row.
|
|
950
|
+
* The domain overlay resolves thousands of these per redraw.
|
|
951
|
+
*/
|
|
952
|
+
readonly seqPosGlobalColIndex: Map<string, Int32Array<ArrayBuffer>>;
|
|
942
953
|
/**
|
|
943
954
|
* #getter
|
|
944
955
|
*/
|
|
@@ -958,11 +969,12 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
|
|
|
958
969
|
/**
|
|
959
970
|
* #getter
|
|
960
971
|
*/
|
|
961
|
-
readonly colStats:
|
|
972
|
+
readonly colStats: import("./columnCounts.ts").ColumnCounts;
|
|
962
973
|
/**
|
|
963
974
|
* #getter
|
|
975
|
+
* per-column residue totals (gaps included)
|
|
964
976
|
*/
|
|
965
|
-
readonly colStatsSums:
|
|
977
|
+
readonly colStatsSums: Uint32Array<ArrayBufferLike>;
|
|
966
978
|
/**
|
|
967
979
|
* #getter
|
|
968
980
|
* Detects sequence type based on letters present in the alignment.
|
|
@@ -1382,20 +1394,50 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
|
|
|
1382
1394
|
end: number;
|
|
1383
1395
|
strand: number | undefined;
|
|
1384
1396
|
}[];
|
|
1397
|
+
/**
|
|
1398
|
+
* #getter
|
|
1399
|
+
* every filtered-on domain annotation resolved to the visible column span
|
|
1400
|
+
* it is drawn across, keyed by row name and in draw order (largest first,
|
|
1401
|
+
* as tidyInterProAnnotations orders them). Resolving these once here rather
|
|
1402
|
+
* than inside each canvas block removes a per-feature, per-block sequence
|
|
1403
|
+
* position conversion from every redraw, and gives the letter renderer the
|
|
1404
|
+
* band colors it needs to keep residues readable on top of the boxes.
|
|
1405
|
+
*/
|
|
1406
|
+
readonly domainBands: Map<string, DomainBand[]>;
|
|
1407
|
+
/**
|
|
1408
|
+
* #getter
|
|
1409
|
+
* the same bands ordered by start column, for left-to-right sweeps (the
|
|
1410
|
+
* letter renderer walks columns and needs the band covering each one)
|
|
1411
|
+
*/
|
|
1412
|
+
readonly domainBandsByStart: Map<string, DomainBand[]>;
|
|
1385
1413
|
/**
|
|
1386
1414
|
* #getter
|
|
1387
1415
|
* domain annotations under the mouse, hit-tested against the exact visible
|
|
1388
1416
|
* column span each box is drawn at (so it matches the overlay across gaps)
|
|
1389
1417
|
*/
|
|
1390
|
-
readonly mouseOverDomains:
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1418
|
+
readonly mouseOverDomains: import("./types.ts").TidyDomainAnnotation[];
|
|
1419
|
+
/**
|
|
1420
|
+
* #getter
|
|
1421
|
+
* row index of the reference row (`relativeTo`), undefined when unset
|
|
1422
|
+
*/
|
|
1423
|
+
readonly referenceRowIndex: number | undefined;
|
|
1424
|
+
/**
|
|
1425
|
+
* #getter
|
|
1426
|
+
* row indices highlighted by the current tree hover (a hovered internal
|
|
1427
|
+
* node highlights every tip below it). Shared by the tree and MSA overlay
|
|
1428
|
+
* canvases so they cannot disagree, and resolved through the memoized
|
|
1429
|
+
* name->index map rather than rebuilding a lookup on each mouse move.
|
|
1430
|
+
*/
|
|
1431
|
+
readonly hoveredRowIndices: number[];
|
|
1432
|
+
/**
|
|
1433
|
+
* #getter
|
|
1434
|
+
* contiguous runs of `highlightedColumns`, so a run of highlighted columns
|
|
1435
|
+
* draws as one bordered band. Computed here because the overlay canvas
|
|
1436
|
+
* redraws on every mouse move while the highlight itself rarely changes.
|
|
1437
|
+
*/
|
|
1438
|
+
readonly highlightedColumnRuns: {
|
|
1396
1439
|
start: number;
|
|
1397
1440
|
end: number;
|
|
1398
|
-
strand: number | undefined;
|
|
1399
1441
|
}[];
|
|
1400
1442
|
/**
|
|
1401
1443
|
* #getter
|
package/dist/model.js
CHANGED
|
@@ -8,6 +8,7 @@ import { generateNodeIds, gffToInterProResults, parseEmfTree, parseGFF, parseMSA
|
|
|
8
8
|
import { calculateBlocks } from "./calculateBlocks.js";
|
|
9
9
|
import { clustalXColumnColors } from "./clustalX.js";
|
|
10
10
|
import colorSchemes from "./colorSchemes.js";
|
|
11
|
+
import { columnCountsFromRows, letterOfResidueSlot } from "./columnCounts.js";
|
|
11
12
|
import ConservationTrack from "./components/ConservationTrack.js";
|
|
12
13
|
import TextTrack from "./components/TextTrack.js";
|
|
13
14
|
import { defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultShowDomains, defaultSubFeatureRows, maxCellSize, minColWidth, minRowHeight, segmentFeatureTypes, segmentShades, } from "./constants.js";
|
|
@@ -24,7 +25,8 @@ import { calculateNeighborJoiningTree } from "./neighborJoining.js";
|
|
|
24
25
|
import { parseAsn1 } from "./parseAsn1.js";
|
|
25
26
|
import { calculatePropertyConservation } from "./propertyConservation.js";
|
|
26
27
|
import { globalColToVisibleCol, visibleColToGlobalCol, visibleColToSeqPosForRow, } from "./rowCoordinateCalculations.js";
|
|
27
|
-
import {
|
|
28
|
+
import { buildSeqPosIndex } from "./seqPosToGlobalCol.js";
|
|
29
|
+
import { stripDefault } from "./stripDefault.js";
|
|
28
30
|
import { computeRowInsertions, len, skipBlanks, transform } from "./util.js";
|
|
29
31
|
import { saveAs } from "./vendor/fileSaver.js";
|
|
30
32
|
function parseTreeText(text) {
|
|
@@ -64,19 +66,19 @@ function stateModelFactory() {
|
|
|
64
66
|
/**
|
|
65
67
|
* #property
|
|
66
68
|
*/
|
|
67
|
-
showDomains:
|
|
69
|
+
showDomains: stripDefault(types.boolean, defaultShowDomains),
|
|
68
70
|
/**
|
|
69
71
|
* #property
|
|
70
72
|
*/
|
|
71
|
-
hideGaps:
|
|
73
|
+
hideGaps: stripDefault(types.boolean, defaultHideGaps),
|
|
72
74
|
/**
|
|
73
75
|
* #property
|
|
74
76
|
*/
|
|
75
|
-
allowedGappyness:
|
|
77
|
+
allowedGappyness: stripDefault(types.number, defaultAllowedGappyness),
|
|
76
78
|
/**
|
|
77
79
|
* #property
|
|
78
80
|
*/
|
|
79
|
-
subFeatureRows:
|
|
81
|
+
subFeatureRows: stripDefault(types.boolean, defaultSubFeatureRows),
|
|
80
82
|
/**
|
|
81
83
|
* #property
|
|
82
84
|
* hardcoded view type
|
|
@@ -85,37 +87,37 @@ function stateModelFactory() {
|
|
|
85
87
|
/**
|
|
86
88
|
* #property
|
|
87
89
|
*/
|
|
88
|
-
drawMsaLetters:
|
|
90
|
+
drawMsaLetters: stripDefault(types.boolean, defaultDrawMsaLetters),
|
|
89
91
|
/**
|
|
90
92
|
* #property
|
|
91
93
|
* zoom in/out on plain mouse-wheel without holding ctrl
|
|
92
94
|
*/
|
|
93
|
-
scrollZoom:
|
|
95
|
+
scrollZoom: stripDefault(types.boolean, defaultScrollZoom),
|
|
94
96
|
/**
|
|
95
97
|
* #property
|
|
96
98
|
* height of the div containing the view, px
|
|
97
99
|
*/
|
|
98
|
-
height:
|
|
100
|
+
height: stripDefault(types.number, defaultHeight),
|
|
99
101
|
/**
|
|
100
102
|
* #property
|
|
101
103
|
* height of each row, px
|
|
102
104
|
*/
|
|
103
|
-
rowHeight:
|
|
105
|
+
rowHeight: stripDefault(types.number, defaultRowHeight),
|
|
104
106
|
/**
|
|
105
107
|
* #property
|
|
106
108
|
* scroll position, Y-offset, px
|
|
107
109
|
*/
|
|
108
|
-
scrollY:
|
|
110
|
+
scrollY: stripDefault(types.number, defaultScrollY),
|
|
109
111
|
/**
|
|
110
112
|
* #property
|
|
111
113
|
* scroll position, X-offset, px
|
|
112
114
|
*/
|
|
113
|
-
scrollX:
|
|
115
|
+
scrollX: stripDefault(types.number, defaultScrollX),
|
|
114
116
|
/**
|
|
115
117
|
* #property
|
|
116
118
|
* width of columns, px
|
|
117
119
|
*/
|
|
118
|
-
colWidth:
|
|
120
|
+
colWidth: stripDefault(types.number, defaultColWidth),
|
|
119
121
|
/**
|
|
120
122
|
* #property
|
|
121
123
|
* filehandle object for the tree
|
|
@@ -140,13 +142,13 @@ function stateModelFactory() {
|
|
|
140
142
|
/**
|
|
141
143
|
* #property
|
|
142
144
|
*/
|
|
143
|
-
currentAlignment:
|
|
145
|
+
currentAlignment: stripDefault(types.number, defaultCurrentAlignment),
|
|
144
146
|
/**
|
|
145
147
|
* #property
|
|
146
148
|
* array of tree parent nodes that are 'collapsed' (all children are
|
|
147
149
|
* hidden)
|
|
148
150
|
*/
|
|
149
|
-
collapsed:
|
|
151
|
+
collapsed: stripDefault(types.array(types.string), []),
|
|
150
152
|
/**
|
|
151
153
|
* #property
|
|
152
154
|
* focus on particular subtree
|
|
@@ -156,7 +158,7 @@ function stateModelFactory() {
|
|
|
156
158
|
* #property
|
|
157
159
|
* turned off tracks
|
|
158
160
|
*/
|
|
159
|
-
turnedOffTracks:
|
|
161
|
+
turnedOffTracks: stripDefault(types.map(types.boolean), {}),
|
|
160
162
|
/**
|
|
161
163
|
* #property
|
|
162
164
|
* data from the loaded tree/msa/treeMetadata, generally loaded by
|
|
@@ -170,7 +172,7 @@ function stateModelFactory() {
|
|
|
170
172
|
/**
|
|
171
173
|
* #property
|
|
172
174
|
*/
|
|
173
|
-
featureFilters:
|
|
175
|
+
featureFilters: stripDefault(types.map(types.boolean), {}),
|
|
174
176
|
/**
|
|
175
177
|
* #property
|
|
176
178
|
*/
|
|
@@ -681,15 +683,19 @@ function stateModelFactory() {
|
|
|
681
683
|
if (strs.length === 0) {
|
|
682
684
|
return [];
|
|
683
685
|
}
|
|
684
|
-
|
|
686
|
+
// ragged input (a3m, hand-edited fasta) can have rows shorter than the
|
|
687
|
+
// alignment; the widest row defines the column count and a row that
|
|
688
|
+
// stops early counts as gapped for the remainder
|
|
689
|
+
const numCols = strs.reduce((max, str) => Math.max(max, str.length), 0);
|
|
685
690
|
const numRows = strs.length;
|
|
686
691
|
const threshold = Math.ceil((realAllowedGappyness / 100) * numRows);
|
|
687
|
-
const blankCounts = new
|
|
692
|
+
const blankCounts = new Uint32Array(numCols);
|
|
688
693
|
for (let j = 0; j < numRows; j++) {
|
|
689
694
|
const str = strs[j];
|
|
695
|
+
const len = str.length;
|
|
690
696
|
for (let i = 0; i < numCols; i++) {
|
|
691
697
|
// bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
|
|
692
|
-
if ((str.charCodeAt(i) - 45) >>> 0 <= 1) {
|
|
698
|
+
if (i >= len || (str.charCodeAt(i) - 45) >>> 0 <= 1) {
|
|
693
699
|
blankCounts[i]++;
|
|
694
700
|
}
|
|
695
701
|
}
|
|
@@ -737,9 +743,22 @@ function stateModelFactory() {
|
|
|
737
743
|
},
|
|
738
744
|
/**
|
|
739
745
|
* #getter
|
|
746
|
+
* number of rows the alignment occupies on screen. This is the leaf count,
|
|
747
|
+
* not `rows.length`: a tree leaf with no matching MSA row still takes up a
|
|
748
|
+
* row of vertical space (drawn blank), so row hit-testing and fit-to-height
|
|
749
|
+
* must count it.
|
|
740
750
|
*/
|
|
741
751
|
get numRows() {
|
|
742
|
-
return this.
|
|
752
|
+
return this.leaves.length;
|
|
753
|
+
},
|
|
754
|
+
/**
|
|
755
|
+
* #getter
|
|
756
|
+
* per-row index of the global column holding each ungapped sequence
|
|
757
|
+
* position, so seqPos -> column is a lookup rather than a scan of the row.
|
|
758
|
+
* The domain overlay resolves thousands of these per redraw.
|
|
759
|
+
*/
|
|
760
|
+
get seqPosGlobalColIndex() {
|
|
761
|
+
return new Map(this.rows.map(([name, seq]) => [name, buildSeqPosIndex(seq)]));
|
|
743
762
|
},
|
|
744
763
|
/**
|
|
745
764
|
* #getter
|
|
@@ -773,22 +792,14 @@ function stateModelFactory() {
|
|
|
773
792
|
* #getter
|
|
774
793
|
*/
|
|
775
794
|
get colStats() {
|
|
776
|
-
|
|
777
|
-
const columns = this.columns2d;
|
|
778
|
-
for (const column of columns) {
|
|
779
|
-
for (let j = 0; j < column.length; j++) {
|
|
780
|
-
const l = (r[j] ??= {});
|
|
781
|
-
const cj = column[j];
|
|
782
|
-
l[cj] = (l[cj] ?? 0) + 1;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
return r;
|
|
795
|
+
return columnCountsFromRows(this.columns2d);
|
|
786
796
|
},
|
|
787
797
|
/**
|
|
788
798
|
* #getter
|
|
799
|
+
* per-column residue totals (gaps included)
|
|
789
800
|
*/
|
|
790
801
|
get colStatsSums() {
|
|
791
|
-
return this.colStats.
|
|
802
|
+
return this.colStats.totals;
|
|
792
803
|
},
|
|
793
804
|
/**
|
|
794
805
|
* #getter
|
|
@@ -796,14 +807,7 @@ function stateModelFactory() {
|
|
|
796
807
|
* Returns 'dna', 'rna', or 'amino'.
|
|
797
808
|
*/
|
|
798
809
|
get sequenceType() {
|
|
799
|
-
const letters =
|
|
800
|
-
for (const stats of this.colStats) {
|
|
801
|
-
for (const letter of Object.keys(stats)) {
|
|
802
|
-
if (letter !== '-' && letter !== '.') {
|
|
803
|
-
letters.add(letter);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
810
|
+
const letters = this.colStats.lettersPresent;
|
|
807
811
|
// isDna already excludes U (not in the DNA set) and isRna excludes T,
|
|
808
812
|
// so the set membership alone disambiguates the two
|
|
809
813
|
const dna = new Set(['A', 'C', 'G', 'T', 'N']);
|
|
@@ -818,21 +822,18 @@ function stateModelFactory() {
|
|
|
818
822
|
* Used by percent_identity_dynamic color scheme.
|
|
819
823
|
*/
|
|
820
824
|
get colConsensus() {
|
|
821
|
-
const { colStats
|
|
822
|
-
return colStats.
|
|
823
|
-
const total =
|
|
824
|
-
if (!total) {
|
|
825
|
-
return { letter: '', color: undefined };
|
|
826
|
-
}
|
|
825
|
+
const { colStats } = this;
|
|
826
|
+
return Array.from({ length: colStats.numColumns }, (_, col) => {
|
|
827
|
+
const total = colStats.total(col);
|
|
827
828
|
let maxCount = 0;
|
|
828
829
|
let letter = '';
|
|
829
|
-
|
|
830
|
-
if (
|
|
831
|
-
maxCount =
|
|
832
|
-
letter =
|
|
830
|
+
colStats.forEachResidue(col, (slot, count) => {
|
|
831
|
+
if (count > maxCount) {
|
|
832
|
+
maxCount = count;
|
|
833
|
+
letter = letterOfResidueSlot(slot);
|
|
833
834
|
}
|
|
834
|
-
}
|
|
835
|
-
const proportion = maxCount / total;
|
|
835
|
+
});
|
|
836
|
+
const proportion = total ? maxCount / total : 0;
|
|
836
837
|
return {
|
|
837
838
|
letter,
|
|
838
839
|
color: proportion > 0.4
|
|
@@ -848,8 +849,8 @@ function stateModelFactory() {
|
|
|
848
849
|
* ref http://www.jalview.org/help/html/colourSchemes/clustal.html
|
|
849
850
|
*/
|
|
850
851
|
get colClustalX() {
|
|
851
|
-
const { colStats
|
|
852
|
-
return colStats.
|
|
852
|
+
const { colStats } = this;
|
|
853
|
+
return Array.from({ length: colStats.numColumns }, (_, col) => clustalXColumnColors(colStats, col));
|
|
853
854
|
},
|
|
854
855
|
/**
|
|
855
856
|
* #getter
|
|
@@ -858,29 +859,24 @@ function stateModelFactory() {
|
|
|
858
859
|
* Returns values 0-1 where 1 = fully conserved, 0 = no conservation.
|
|
859
860
|
*/
|
|
860
861
|
get conservation() {
|
|
861
|
-
const { colStats,
|
|
862
|
+
const { colStats, sequenceType } = this;
|
|
862
863
|
const alphabetSize = sequenceType === 'amino' ? 20 : 4;
|
|
863
864
|
const maxEntropy = Math.log2(alphabetSize);
|
|
864
|
-
return colStats.
|
|
865
|
-
const total =
|
|
866
|
-
|
|
867
|
-
return 0;
|
|
868
|
-
}
|
|
869
|
-
const gapCount = (stats['-'] ?? 0) + (stats['.'] ?? 0);
|
|
865
|
+
return Array.from({ length: colStats.numColumns }, (_, col) => {
|
|
866
|
+
const total = colStats.total(col);
|
|
867
|
+
const gapCount = colStats.gapCount(col);
|
|
870
868
|
const nonGapTotal = total - gapCount;
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
for (const [letter, count] of Object.entries(stats)) {
|
|
876
|
-
if (letter !== '-' && letter !== '.') {
|
|
869
|
+
let score = 0;
|
|
870
|
+
if (nonGapTotal > 0) {
|
|
871
|
+
let entropy = 0;
|
|
872
|
+
colStats.forEachResidue(col, (_slot, count) => {
|
|
877
873
|
const freq = count / nonGapTotal;
|
|
878
874
|
entropy -= freq * Math.log2(freq);
|
|
879
|
-
}
|
|
875
|
+
});
|
|
876
|
+
score =
|
|
877
|
+
Math.max(0, 1 - entropy / maxEntropy) * (1 - gapCount / total);
|
|
880
878
|
}
|
|
881
|
-
|
|
882
|
-
const conservation = Math.max(0, 1 - entropy / maxEntropy);
|
|
883
|
-
return conservation * (1 - gapFraction);
|
|
879
|
+
return score;
|
|
884
880
|
});
|
|
885
881
|
},
|
|
886
882
|
/**
|
|
@@ -891,7 +887,7 @@ function stateModelFactory() {
|
|
|
891
887
|
*/
|
|
892
888
|
get propertyConservation() {
|
|
893
889
|
return this.sequenceType === 'amino'
|
|
894
|
-
? calculatePropertyConservation(this.colStats
|
|
890
|
+
? calculatePropertyConservation(this.colStats)
|
|
895
891
|
: [];
|
|
896
892
|
},
|
|
897
893
|
/**
|
|
@@ -1372,7 +1368,13 @@ function stateModelFactory() {
|
|
|
1372
1368
|
*/
|
|
1373
1369
|
seqPosToGlobalCol(rowName, seqPos) {
|
|
1374
1370
|
const seq = self.rowMap.get(rowName);
|
|
1375
|
-
|
|
1371
|
+
if (seq === undefined) {
|
|
1372
|
+
return 0;
|
|
1373
|
+
}
|
|
1374
|
+
const col = self.seqPosGlobalColIndex.get(rowName)?.[seqPos];
|
|
1375
|
+
// past the end of the ungapped sequence: 0 for the degenerate empty-row
|
|
1376
|
+
// case, otherwise one past the last column, as the scan version returned
|
|
1377
|
+
return col ?? (seqPos === 0 ? 0 : seq.length);
|
|
1376
1378
|
},
|
|
1377
1379
|
/**
|
|
1378
1380
|
* #method
|
|
@@ -1507,6 +1509,48 @@ function stateModelFactory() {
|
|
|
1507
1509
|
.filter(d => self.featureFilters.get(d.accession))
|
|
1508
1510
|
.toSorted((a, b) => a.start - b.start);
|
|
1509
1511
|
},
|
|
1512
|
+
/**
|
|
1513
|
+
* #getter
|
|
1514
|
+
* every filtered-on domain annotation resolved to the visible column span
|
|
1515
|
+
* it is drawn across, keyed by row name and in draw order (largest first,
|
|
1516
|
+
* as tidyInterProAnnotations orders them). Resolving these once here rather
|
|
1517
|
+
* than inside each canvas block removes a per-feature, per-block sequence
|
|
1518
|
+
* position conversion from every redraw, and gives the letter renderer the
|
|
1519
|
+
* band colors it needs to keep residues readable on top of the boxes.
|
|
1520
|
+
*/
|
|
1521
|
+
get domainBands() {
|
|
1522
|
+
const bands = new Map();
|
|
1523
|
+
for (const [name, annotations] of Object.entries(self.tidyFilteredGatheredInterProAnnotations)) {
|
|
1524
|
+
const rowBands = annotations
|
|
1525
|
+
.map((annotation, stackIndex) => {
|
|
1526
|
+
// InterPro positions are 1-based and inclusive; endCol is the
|
|
1527
|
+
// exclusive column *after* the last residue, taken from that
|
|
1528
|
+
// residue rather than from the next one, so a band stops at its own
|
|
1529
|
+
// last residue instead of stretching across a following gap run
|
|
1530
|
+
const startCol = self.seqPosToVisibleCol(name, annotation.start - 1);
|
|
1531
|
+
const lastCol = self.seqPosToVisibleCol(name, annotation.end - 1);
|
|
1532
|
+
return startCol !== undefined && lastCol !== undefined
|
|
1533
|
+
? { annotation, startCol, endCol: lastCol + 1, stackIndex }
|
|
1534
|
+
: undefined;
|
|
1535
|
+
})
|
|
1536
|
+
.filter(notEmpty);
|
|
1537
|
+
if (rowBands.length > 0) {
|
|
1538
|
+
bands.set(name, rowBands);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return bands;
|
|
1542
|
+
},
|
|
1543
|
+
/**
|
|
1544
|
+
* #getter
|
|
1545
|
+
* the same bands ordered by start column, for left-to-right sweeps (the
|
|
1546
|
+
* letter renderer walks columns and needs the band covering each one)
|
|
1547
|
+
*/
|
|
1548
|
+
get domainBandsByStart() {
|
|
1549
|
+
return new Map([...this.domainBands].map(([name, bands]) => [
|
|
1550
|
+
name,
|
|
1551
|
+
bands.toSorted((a, b) => a.startCol - b.startCol),
|
|
1552
|
+
]));
|
|
1553
|
+
},
|
|
1510
1554
|
/**
|
|
1511
1555
|
* #getter
|
|
1512
1556
|
* domain annotations under the mouse, hit-tested against the exact visible
|
|
@@ -1515,18 +1559,56 @@ function stateModelFactory() {
|
|
|
1515
1559
|
get mouseOverDomains() {
|
|
1516
1560
|
const { mouseCol } = self;
|
|
1517
1561
|
const name = self.mouseOverRowName;
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1562
|
+
return name !== undefined && mouseCol !== undefined
|
|
1563
|
+
? (this.domainBands.get(name) ?? [])
|
|
1564
|
+
.filter(b => mouseCol >= b.startCol && mouseCol < b.endCol)
|
|
1565
|
+
.map(b => b.annotation)
|
|
1566
|
+
: [];
|
|
1567
|
+
},
|
|
1568
|
+
/**
|
|
1569
|
+
* #getter
|
|
1570
|
+
* row index of the reference row (`relativeTo`), undefined when unset
|
|
1571
|
+
*/
|
|
1572
|
+
get referenceRowIndex() {
|
|
1573
|
+
const { relativeTo } = self;
|
|
1574
|
+
return relativeTo === undefined
|
|
1575
|
+
? undefined
|
|
1576
|
+
: self.rowNamesSet.get(relativeTo);
|
|
1577
|
+
},
|
|
1578
|
+
/**
|
|
1579
|
+
* #getter
|
|
1580
|
+
* row indices highlighted by the current tree hover (a hovered internal
|
|
1581
|
+
* node highlights every tip below it). Shared by the tree and MSA overlay
|
|
1582
|
+
* canvases so they cannot disagree, and resolved through the memoized
|
|
1583
|
+
* name->index map rather than rebuilding a lookup on each mouse move.
|
|
1584
|
+
*/
|
|
1585
|
+
get hoveredRowIndices() {
|
|
1586
|
+
const { hoveredTreeNode, rowNamesSet } = self;
|
|
1587
|
+
return hoveredTreeNode
|
|
1588
|
+
? hoveredTreeNode.descendantNames
|
|
1589
|
+
.map(name => rowNamesSet.get(name))
|
|
1590
|
+
.filter(notEmpty)
|
|
1591
|
+
: [];
|
|
1592
|
+
},
|
|
1593
|
+
/**
|
|
1594
|
+
* #getter
|
|
1595
|
+
* contiguous runs of `highlightedColumns`, so a run of highlighted columns
|
|
1596
|
+
* draws as one bordered band. Computed here because the overlay canvas
|
|
1597
|
+
* redraws on every mouse move while the highlight itself rarely changes.
|
|
1598
|
+
*/
|
|
1599
|
+
get highlightedColumnRuns() {
|
|
1600
|
+
const { highlightedColumns } = self;
|
|
1601
|
+
const runs = [];
|
|
1602
|
+
for (const col of [...(highlightedColumns ?? [])].sort((a, b) => a - b)) {
|
|
1603
|
+
const last = runs.at(-1);
|
|
1604
|
+
if (last && col === last.end + 1) {
|
|
1605
|
+
last.end = col;
|
|
1606
|
+
}
|
|
1607
|
+
else {
|
|
1608
|
+
runs.push({ start: col, end: col });
|
|
1609
|
+
}
|
|
1528
1610
|
}
|
|
1529
|
-
return
|
|
1611
|
+
return runs;
|
|
1530
1612
|
},
|
|
1531
1613
|
/**
|
|
1532
1614
|
* #getter
|
|
@@ -1539,14 +1621,14 @@ function stateModelFactory() {
|
|
|
1539
1621
|
if (mouseCol === undefined) {
|
|
1540
1622
|
return undefined;
|
|
1541
1623
|
}
|
|
1542
|
-
const
|
|
1543
|
-
const total =
|
|
1544
|
-
if (
|
|
1624
|
+
const { colStats } = self;
|
|
1625
|
+
const total = colStats.total(mouseCol);
|
|
1626
|
+
if (mouseCol >= colStats.numColumns || !total) {
|
|
1545
1627
|
return undefined;
|
|
1546
1628
|
}
|
|
1547
|
-
const gaps = (
|
|
1548
|
-
const distribution =
|
|
1549
|
-
.
|
|
1629
|
+
const gaps = colStats.gapCount(mouseCol);
|
|
1630
|
+
const distribution = colStats
|
|
1631
|
+
.residueEntries(mouseCol)
|
|
1550
1632
|
.sort((a, b) => b[1] - a[1]);
|
|
1551
1633
|
const consensus = distribution[0];
|
|
1552
1634
|
return {
|
|
@@ -1657,6 +1739,25 @@ function stateModelFactory() {
|
|
|
1657
1739
|
this.initFilter(key);
|
|
1658
1740
|
}
|
|
1659
1741
|
}));
|
|
1742
|
+
// track the live device pixel ratio so canvas backing stores re-scale
|
|
1743
|
+
// when the window moves between monitors or the browser zooms. The
|
|
1744
|
+
// matchMedia query is pinned to the current ratio, so each change
|
|
1745
|
+
// re-registers against the new one to keep tracking further moves.
|
|
1746
|
+
if (typeof window !== 'undefined' &&
|
|
1747
|
+
typeof window.matchMedia === 'function') {
|
|
1748
|
+
const query = () => window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
1749
|
+
let mql = query();
|
|
1750
|
+
const onChange = () => {
|
|
1751
|
+
self.setHighResScaleFactor(window.devicePixelRatio);
|
|
1752
|
+
mql.removeEventListener('change', onChange);
|
|
1753
|
+
mql = query();
|
|
1754
|
+
mql.addEventListener('change', onChange);
|
|
1755
|
+
};
|
|
1756
|
+
mql.addEventListener('change', onChange);
|
|
1757
|
+
addDisposer(self, () => {
|
|
1758
|
+
mql.removeEventListener('change', onChange);
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1660
1761
|
// autorun opens treeFilehandle. generation guard: if the filehandle
|
|
1661
1762
|
// changes mid-fetch, a slower earlier request must not clobber the data
|
|
1662
1763
|
// (or loading flag) set by the newer one
|
|
@@ -1804,16 +1905,30 @@ function stateModelFactory() {
|
|
|
1804
1905
|
}));
|
|
1805
1906
|
// Keep the parse chain warm: reading self.columns transitively holds
|
|
1806
1907
|
// self.MSA (parseMSA) computed alive, so it is parsed once per data
|
|
1807
|
-
// change rather than re-parsed on every non-reactive access.
|
|
1808
|
-
//
|
|
1908
|
+
// change rather than re-parsed on every non-reactive access. Do not
|
|
1909
|
+
// remove.
|
|
1809
1910
|
// xref solution https://github.com/mobxjs/mobx/issues/266#issuecomment-222007278
|
|
1810
1911
|
// xref problem https://github.com/GMOD/react-msaview/issues/75
|
|
1912
|
+
//
|
|
1913
|
+
// The column statistics are held for the same reason whenever something
|
|
1914
|
+
// reads them off the reactive path: dynamic color schemes, and the hover
|
|
1915
|
+
// tooltip, which is read from a mousemove handler. Without this the
|
|
1916
|
+
// tooltip's cost depends on whether some visible track happens to be
|
|
1917
|
+
// observing them, so closing the conservation track would silently turn
|
|
1918
|
+
// every mouse move into a full-alignment recount.
|
|
1811
1919
|
addDisposer(self, autorun(() => {
|
|
1812
1920
|
if (self.colorSchemeName.includes('dynamic')) {
|
|
1813
1921
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
1814
1922
|
self.colStats;
|
|
1923
|
+
}
|
|
1924
|
+
if (self.showColumnStats) {
|
|
1925
|
+
// everything the hover tooltip reads per column
|
|
1926
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
1927
|
+
self.colStats;
|
|
1928
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
1929
|
+
self.conservation;
|
|
1815
1930
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
1816
|
-
self.
|
|
1931
|
+
self.propertyConservation;
|
|
1817
1932
|
}
|
|
1818
1933
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
1819
1934
|
self.columns;
|
|
@@ -1836,7 +1951,7 @@ function stateModelFactory() {
|
|
|
1836
1951
|
},
|
|
1837
1952
|
}))
|
|
1838
1953
|
.postProcessSnapshot(({ data, ...rest }) => ({
|
|
1839
|
-
// per-property defaults are stripped by
|
|
1954
|
+
// per-property defaults are stripped by the stripDefault helper; the only thing
|
|
1840
1955
|
// it can't express is this cross-field rule: drop inline tree/msa/metadata
|
|
1841
1956
|
// when a sibling filehandle can refetch them, keeping sessions/URLs small
|
|
1842
1957
|
...rest,
|