react-msaview 5.6.0 → 5.6.2
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/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/colorSchemes.docs.test.ts +3 -1
- 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/model/msaModel.ts +4 -3
- package/src/model/treeModel.ts +9 -8
- package/src/model.ts +241 -106
- 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/stripDefaultSnapshot.test.ts +4 -1
- 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/src/model.ts
CHANGED
|
@@ -13,9 +13,11 @@ import {
|
|
|
13
13
|
parseNewick,
|
|
14
14
|
} from 'msa-parsers'
|
|
15
15
|
|
|
16
|
+
|
|
16
17
|
import { calculateBlocks } from './calculateBlocks.ts'
|
|
17
18
|
import { clustalXColumnColors } from './clustalX.ts'
|
|
18
19
|
import colorSchemes from './colorSchemes.ts'
|
|
20
|
+
import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.ts'
|
|
19
21
|
import ConservationTrack from './components/ConservationTrack.tsx'
|
|
20
22
|
import TextTrack from './components/TextTrack.tsx'
|
|
21
23
|
import {
|
|
@@ -70,13 +72,19 @@ import {
|
|
|
70
72
|
visibleColToGlobalCol,
|
|
71
73
|
visibleColToSeqPosForRow,
|
|
72
74
|
} from './rowCoordinateCalculations.ts'
|
|
73
|
-
import {
|
|
75
|
+
import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
|
|
76
|
+
import { stripDefault } from './stripDefault.ts'
|
|
74
77
|
import { computeRowInsertions, len, skipBlanks, transform } from './util.ts'
|
|
75
78
|
import { saveAs } from './vendor/fileSaver.ts'
|
|
76
79
|
|
|
77
80
|
import type { HierarchyNode } from './hierarchy.ts'
|
|
78
81
|
import type { InterProScanResults } from './launchInterProScan.ts'
|
|
79
|
-
import type {
|
|
82
|
+
import type {
|
|
83
|
+
BasicTrack,
|
|
84
|
+
DomainBand,
|
|
85
|
+
NodeWithIds,
|
|
86
|
+
NodeWithIdsAndLength,
|
|
87
|
+
} from './types.ts'
|
|
80
88
|
import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
|
|
81
89
|
import type { Instance } from '@jbrowse/mobx-state-tree'
|
|
82
90
|
import type { Theme } from '@mui/material'
|
|
@@ -124,22 +132,19 @@ function stateModelFactory() {
|
|
|
124
132
|
/**
|
|
125
133
|
* #property
|
|
126
134
|
*/
|
|
127
|
-
showDomains:
|
|
135
|
+
showDomains: stripDefault(types.boolean, defaultShowDomains),
|
|
128
136
|
/**
|
|
129
137
|
* #property
|
|
130
138
|
*/
|
|
131
|
-
hideGaps:
|
|
139
|
+
hideGaps: stripDefault(types.boolean, defaultHideGaps),
|
|
132
140
|
/**
|
|
133
141
|
* #property
|
|
134
142
|
*/
|
|
135
|
-
allowedGappyness: types.
|
|
136
|
-
types.number,
|
|
137
|
-
defaultAllowedGappyness,
|
|
138
|
-
),
|
|
143
|
+
allowedGappyness: stripDefault(types.number, defaultAllowedGappyness),
|
|
139
144
|
/**
|
|
140
145
|
* #property
|
|
141
146
|
*/
|
|
142
|
-
subFeatureRows:
|
|
147
|
+
subFeatureRows: stripDefault(types.boolean, defaultSubFeatureRows),
|
|
143
148
|
|
|
144
149
|
/**
|
|
145
150
|
* #property
|
|
@@ -150,43 +155,43 @@ function stateModelFactory() {
|
|
|
150
155
|
/**
|
|
151
156
|
* #property
|
|
152
157
|
*/
|
|
153
|
-
drawMsaLetters:
|
|
158
|
+
drawMsaLetters: stripDefault(types.boolean, defaultDrawMsaLetters),
|
|
154
159
|
|
|
155
160
|
/**
|
|
156
161
|
* #property
|
|
157
162
|
* zoom in/out on plain mouse-wheel without holding ctrl
|
|
158
163
|
*/
|
|
159
|
-
scrollZoom:
|
|
164
|
+
scrollZoom: stripDefault(types.boolean, defaultScrollZoom),
|
|
160
165
|
|
|
161
166
|
/**
|
|
162
167
|
* #property
|
|
163
168
|
* height of the div containing the view, px
|
|
164
169
|
*/
|
|
165
|
-
height:
|
|
170
|
+
height: stripDefault(types.number, defaultHeight),
|
|
166
171
|
|
|
167
172
|
/**
|
|
168
173
|
* #property
|
|
169
174
|
* height of each row, px
|
|
170
175
|
*/
|
|
171
|
-
rowHeight:
|
|
176
|
+
rowHeight: stripDefault(types.number, defaultRowHeight),
|
|
172
177
|
|
|
173
178
|
/**
|
|
174
179
|
* #property
|
|
175
180
|
* scroll position, Y-offset, px
|
|
176
181
|
*/
|
|
177
|
-
scrollY:
|
|
182
|
+
scrollY: stripDefault(types.number, defaultScrollY),
|
|
178
183
|
|
|
179
184
|
/**
|
|
180
185
|
* #property
|
|
181
186
|
* scroll position, X-offset, px
|
|
182
187
|
*/
|
|
183
|
-
scrollX:
|
|
188
|
+
scrollX: stripDefault(types.number, defaultScrollX),
|
|
184
189
|
|
|
185
190
|
/**
|
|
186
191
|
* #property
|
|
187
192
|
* width of columns, px
|
|
188
193
|
*/
|
|
189
|
-
colWidth:
|
|
194
|
+
colWidth: stripDefault(types.number, defaultColWidth),
|
|
190
195
|
|
|
191
196
|
/**
|
|
192
197
|
* #property
|
|
@@ -216,17 +221,14 @@ function stateModelFactory() {
|
|
|
216
221
|
/**
|
|
217
222
|
* #property
|
|
218
223
|
*/
|
|
219
|
-
currentAlignment: types.
|
|
220
|
-
types.number,
|
|
221
|
-
defaultCurrentAlignment,
|
|
222
|
-
),
|
|
224
|
+
currentAlignment: stripDefault(types.number, defaultCurrentAlignment),
|
|
223
225
|
|
|
224
226
|
/**
|
|
225
227
|
* #property
|
|
226
228
|
* array of tree parent nodes that are 'collapsed' (all children are
|
|
227
229
|
* hidden)
|
|
228
230
|
*/
|
|
229
|
-
collapsed:
|
|
231
|
+
collapsed: stripDefault(types.array(types.string), []),
|
|
230
232
|
|
|
231
233
|
/**
|
|
232
234
|
* #property
|
|
@@ -237,7 +239,7 @@ function stateModelFactory() {
|
|
|
237
239
|
* #property
|
|
238
240
|
* turned off tracks
|
|
239
241
|
*/
|
|
240
|
-
turnedOffTracks:
|
|
242
|
+
turnedOffTracks: stripDefault(types.map(types.boolean), {}),
|
|
241
243
|
|
|
242
244
|
/**
|
|
243
245
|
* #property
|
|
@@ -253,7 +255,7 @@ function stateModelFactory() {
|
|
|
253
255
|
/**
|
|
254
256
|
* #property
|
|
255
257
|
*/
|
|
256
|
-
featureFilters:
|
|
258
|
+
featureFilters: stripDefault(types.map(types.boolean), {}),
|
|
257
259
|
/**
|
|
258
260
|
* #property
|
|
259
261
|
*/
|
|
@@ -820,15 +822,19 @@ function stateModelFactory() {
|
|
|
820
822
|
if (strs.length === 0) {
|
|
821
823
|
return []
|
|
822
824
|
}
|
|
823
|
-
|
|
825
|
+
// ragged input (a3m, hand-edited fasta) can have rows shorter than the
|
|
826
|
+
// alignment; the widest row defines the column count and a row that
|
|
827
|
+
// stops early counts as gapped for the remainder
|
|
828
|
+
const numCols = strs.reduce((max, str) => Math.max(max, str.length), 0)
|
|
824
829
|
const numRows = strs.length
|
|
825
830
|
const threshold = Math.ceil((realAllowedGappyness / 100) * numRows)
|
|
826
|
-
const blankCounts = new
|
|
831
|
+
const blankCounts = new Uint32Array(numCols)
|
|
827
832
|
for (let j = 0; j < numRows; j++) {
|
|
828
833
|
const str = strs[j]!
|
|
834
|
+
const len = str.length
|
|
829
835
|
for (let i = 0; i < numCols; i++) {
|
|
830
836
|
// bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
|
|
831
|
-
if ((str.charCodeAt(i) - 45) >>> 0 <= 1) {
|
|
837
|
+
if (i >= len || (str.charCodeAt(i) - 45) >>> 0 <= 1) {
|
|
832
838
|
blankCounts[i]!++
|
|
833
839
|
}
|
|
834
840
|
}
|
|
@@ -876,9 +882,27 @@ function stateModelFactory() {
|
|
|
876
882
|
},
|
|
877
883
|
/**
|
|
878
884
|
* #getter
|
|
885
|
+
* number of rows the alignment occupies on screen. This is the leaf count,
|
|
886
|
+
* not `rows.length`: a tree leaf with no matching MSA row still takes up a
|
|
887
|
+
* row of vertical space (drawn blank), so row hit-testing and fit-to-height
|
|
888
|
+
* must count it.
|
|
879
889
|
*/
|
|
880
890
|
get numRows() {
|
|
881
|
-
return this.
|
|
891
|
+
return this.leaves.length
|
|
892
|
+
},
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* #getter
|
|
896
|
+
* per-row index of the global column holding each ungapped sequence
|
|
897
|
+
* position, so seqPos -> column is a lookup rather than a scan of the row.
|
|
898
|
+
* The domain overlay resolves thousands of these per redraw.
|
|
899
|
+
*/
|
|
900
|
+
get seqPosGlobalColIndex() {
|
|
901
|
+
return new Map(
|
|
902
|
+
this.rows.map(
|
|
903
|
+
([name, seq]) => [name, buildSeqPosIndex(seq)] as const,
|
|
904
|
+
),
|
|
905
|
+
)
|
|
882
906
|
},
|
|
883
907
|
|
|
884
908
|
/**
|
|
@@ -918,23 +942,15 @@ function stateModelFactory() {
|
|
|
918
942
|
* #getter
|
|
919
943
|
*/
|
|
920
944
|
get colStats() {
|
|
921
|
-
|
|
922
|
-
const columns = this.columns2d
|
|
923
|
-
for (const column of columns) {
|
|
924
|
-
for (let j = 0; j < column.length; j++) {
|
|
925
|
-
const l = (r[j] ??= {})
|
|
926
|
-
const cj = column[j]!
|
|
927
|
-
l[cj] = (l[cj] ?? 0) + 1
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
return r
|
|
945
|
+
return columnCountsFromRows(this.columns2d)
|
|
931
946
|
},
|
|
932
947
|
|
|
933
948
|
/**
|
|
934
949
|
* #getter
|
|
950
|
+
* per-column residue totals (gaps included)
|
|
935
951
|
*/
|
|
936
952
|
get colStatsSums() {
|
|
937
|
-
return this.colStats.
|
|
953
|
+
return this.colStats.totals
|
|
938
954
|
},
|
|
939
955
|
|
|
940
956
|
/**
|
|
@@ -943,14 +959,7 @@ function stateModelFactory() {
|
|
|
943
959
|
* Returns 'dna', 'rna', or 'amino'.
|
|
944
960
|
*/
|
|
945
961
|
get sequenceType(): 'dna' | 'rna' | 'amino' {
|
|
946
|
-
const letters =
|
|
947
|
-
for (const stats of this.colStats) {
|
|
948
|
-
for (const letter of Object.keys(stats)) {
|
|
949
|
-
if (letter !== '-' && letter !== '.') {
|
|
950
|
-
letters.add(letter)
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
}
|
|
962
|
+
const letters = this.colStats.lettersPresent
|
|
954
963
|
// isDna already excludes U (not in the DNA set) and isRna excludes T,
|
|
955
964
|
// so the set membership alone disambiguates the two
|
|
956
965
|
const dna = new Set(['A', 'C', 'G', 'T', 'N'])
|
|
@@ -966,21 +975,18 @@ function stateModelFactory() {
|
|
|
966
975
|
* Used by percent_identity_dynamic color scheme.
|
|
967
976
|
*/
|
|
968
977
|
get colConsensus() {
|
|
969
|
-
const { colStats
|
|
970
|
-
return colStats.
|
|
971
|
-
const total =
|
|
972
|
-
if (!total) {
|
|
973
|
-
return { letter: '', color: undefined }
|
|
974
|
-
}
|
|
978
|
+
const { colStats } = this
|
|
979
|
+
return Array.from({ length: colStats.numColumns }, (_, col) => {
|
|
980
|
+
const total = colStats.total(col)
|
|
975
981
|
let maxCount = 0
|
|
976
982
|
let letter = ''
|
|
977
|
-
|
|
978
|
-
if (
|
|
979
|
-
maxCount =
|
|
980
|
-
letter =
|
|
983
|
+
colStats.forEachResidue(col, (slot, count) => {
|
|
984
|
+
if (count > maxCount) {
|
|
985
|
+
maxCount = count
|
|
986
|
+
letter = letterOfResidueSlot(slot)
|
|
981
987
|
}
|
|
982
|
-
}
|
|
983
|
-
const proportion = maxCount / total
|
|
988
|
+
})
|
|
989
|
+
const proportion = total ? maxCount / total : 0
|
|
984
990
|
return {
|
|
985
991
|
letter,
|
|
986
992
|
color:
|
|
@@ -998,9 +1004,9 @@ function stateModelFactory() {
|
|
|
998
1004
|
* ref http://www.jalview.org/help/html/colourSchemes/clustal.html
|
|
999
1005
|
*/
|
|
1000
1006
|
get colClustalX() {
|
|
1001
|
-
const { colStats
|
|
1002
|
-
return colStats.
|
|
1003
|
-
clustalXColumnColors(
|
|
1007
|
+
const { colStats } = this
|
|
1008
|
+
return Array.from({ length: colStats.numColumns }, (_, col) =>
|
|
1009
|
+
clustalXColumnColors(colStats, col),
|
|
1004
1010
|
)
|
|
1005
1011
|
},
|
|
1006
1012
|
|
|
@@ -1011,33 +1017,25 @@ function stateModelFactory() {
|
|
|
1011
1017
|
* Returns values 0-1 where 1 = fully conserved, 0 = no conservation.
|
|
1012
1018
|
*/
|
|
1013
1019
|
get conservation() {
|
|
1014
|
-
const { colStats,
|
|
1020
|
+
const { colStats, sequenceType } = this
|
|
1015
1021
|
const alphabetSize = sequenceType === 'amino' ? 20 : 4
|
|
1016
1022
|
const maxEntropy = Math.log2(alphabetSize)
|
|
1017
1023
|
|
|
1018
|
-
return colStats.
|
|
1019
|
-
const total =
|
|
1020
|
-
|
|
1021
|
-
return 0
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
const gapCount = (stats['-'] ?? 0) + (stats['.'] ?? 0)
|
|
1024
|
+
return Array.from({ length: colStats.numColumns }, (_, col) => {
|
|
1025
|
+
const total = colStats.total(col)
|
|
1026
|
+
const gapCount = colStats.gapCount(col)
|
|
1025
1027
|
const nonGapTotal = total - gapCount
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
let entropy = 0
|
|
1031
|
-
for (const [letter, count] of Object.entries(stats)) {
|
|
1032
|
-
if (letter !== '-' && letter !== '.') {
|
|
1028
|
+
let score = 0
|
|
1029
|
+
if (nonGapTotal > 0) {
|
|
1030
|
+
let entropy = 0
|
|
1031
|
+
colStats.forEachResidue(col, (_slot, count) => {
|
|
1033
1032
|
const freq = count / nonGapTotal
|
|
1034
1033
|
entropy -= freq * Math.log2(freq)
|
|
1035
|
-
}
|
|
1034
|
+
})
|
|
1035
|
+
score =
|
|
1036
|
+
Math.max(0, 1 - entropy / maxEntropy) * (1 - gapCount / total)
|
|
1036
1037
|
}
|
|
1037
|
-
|
|
1038
|
-
const gapFraction = gapCount / total
|
|
1039
|
-
const conservation = Math.max(0, 1 - entropy / maxEntropy)
|
|
1040
|
-
return conservation * (1 - gapFraction)
|
|
1038
|
+
return score
|
|
1041
1039
|
})
|
|
1042
1040
|
},
|
|
1043
1041
|
/**
|
|
@@ -1048,7 +1046,7 @@ function stateModelFactory() {
|
|
|
1048
1046
|
*/
|
|
1049
1047
|
get propertyConservation() {
|
|
1050
1048
|
return this.sequenceType === 'amino'
|
|
1051
|
-
? calculatePropertyConservation(this.colStats
|
|
1049
|
+
? calculatePropertyConservation(this.colStats)
|
|
1052
1050
|
: []
|
|
1053
1051
|
},
|
|
1054
1052
|
/**
|
|
@@ -1584,7 +1582,13 @@ function stateModelFactory() {
|
|
|
1584
1582
|
*/
|
|
1585
1583
|
seqPosToGlobalCol(rowName: string, seqPos: number) {
|
|
1586
1584
|
const seq = self.rowMap.get(rowName)
|
|
1587
|
-
|
|
1585
|
+
if (seq === undefined) {
|
|
1586
|
+
return 0
|
|
1587
|
+
}
|
|
1588
|
+
const col = self.seqPosGlobalColIndex.get(rowName)?.[seqPos]
|
|
1589
|
+
// past the end of the ungapped sequence: 0 for the degenerate empty-row
|
|
1590
|
+
// case, otherwise one past the last column, as the scan version returned
|
|
1591
|
+
return col ?? (seqPos === 0 ? 0 : seq.length)
|
|
1588
1592
|
},
|
|
1589
1593
|
|
|
1590
1594
|
/**
|
|
@@ -1740,6 +1744,60 @@ function stateModelFactory() {
|
|
|
1740
1744
|
.toSorted((a, b) => a.start - b.start)
|
|
1741
1745
|
},
|
|
1742
1746
|
|
|
1747
|
+
/**
|
|
1748
|
+
* #getter
|
|
1749
|
+
* every filtered-on domain annotation resolved to the visible column span
|
|
1750
|
+
* it is drawn across, keyed by row name and in draw order (largest first,
|
|
1751
|
+
* as tidyInterProAnnotations orders them). Resolving these once here rather
|
|
1752
|
+
* than inside each canvas block removes a per-feature, per-block sequence
|
|
1753
|
+
* position conversion from every redraw, and gives the letter renderer the
|
|
1754
|
+
* band colors it needs to keep residues readable on top of the boxes.
|
|
1755
|
+
*/
|
|
1756
|
+
get domainBands() {
|
|
1757
|
+
const bands = new Map<string, DomainBand[]>()
|
|
1758
|
+
for (const [name, annotations] of Object.entries(
|
|
1759
|
+
self.tidyFilteredGatheredInterProAnnotations,
|
|
1760
|
+
)) {
|
|
1761
|
+
const rowBands = annotations
|
|
1762
|
+
.map((annotation, stackIndex) => {
|
|
1763
|
+
// InterPro positions are 1-based and inclusive; endCol is the
|
|
1764
|
+
// exclusive column *after* the last residue, taken from that
|
|
1765
|
+
// residue rather than from the next one, so a band stops at its own
|
|
1766
|
+
// last residue instead of stretching across a following gap run
|
|
1767
|
+
const startCol = self.seqPosToVisibleCol(
|
|
1768
|
+
name,
|
|
1769
|
+
annotation.start - 1,
|
|
1770
|
+
)
|
|
1771
|
+
const lastCol = self.seqPosToVisibleCol(name, annotation.end - 1)
|
|
1772
|
+
return startCol !== undefined && lastCol !== undefined
|
|
1773
|
+
? { annotation, startCol, endCol: lastCol + 1, stackIndex }
|
|
1774
|
+
: undefined
|
|
1775
|
+
})
|
|
1776
|
+
.filter(notEmpty)
|
|
1777
|
+
if (rowBands.length > 0) {
|
|
1778
|
+
bands.set(name, rowBands)
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return bands
|
|
1782
|
+
},
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* #getter
|
|
1786
|
+
* the same bands ordered by start column, for left-to-right sweeps (the
|
|
1787
|
+
* letter renderer walks columns and needs the band covering each one)
|
|
1788
|
+
*/
|
|
1789
|
+
get domainBandsByStart() {
|
|
1790
|
+
return new Map(
|
|
1791
|
+
[...this.domainBands].map(
|
|
1792
|
+
([name, bands]) =>
|
|
1793
|
+
[
|
|
1794
|
+
name,
|
|
1795
|
+
bands.toSorted((a, b) => a.startCol - b.startCol),
|
|
1796
|
+
] as const,
|
|
1797
|
+
),
|
|
1798
|
+
)
|
|
1799
|
+
},
|
|
1800
|
+
|
|
1743
1801
|
/**
|
|
1744
1802
|
* #getter
|
|
1745
1803
|
* domain annotations under the mouse, hit-tested against the exact visible
|
|
@@ -1748,20 +1806,60 @@ function stateModelFactory() {
|
|
|
1748
1806
|
get mouseOverDomains() {
|
|
1749
1807
|
const { mouseCol } = self
|
|
1750
1808
|
const name = self.mouseOverRowName
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1809
|
+
return name !== undefined && mouseCol !== undefined
|
|
1810
|
+
? (this.domainBands.get(name) ?? [])
|
|
1811
|
+
.filter(b => mouseCol >= b.startCol && mouseCol < b.endCol)
|
|
1812
|
+
.map(b => b.annotation)
|
|
1813
|
+
: []
|
|
1814
|
+
},
|
|
1815
|
+
|
|
1816
|
+
/**
|
|
1817
|
+
* #getter
|
|
1818
|
+
* row index of the reference row (`relativeTo`), undefined when unset
|
|
1819
|
+
*/
|
|
1820
|
+
get referenceRowIndex() {
|
|
1821
|
+
const { relativeTo } = self
|
|
1822
|
+
return relativeTo === undefined
|
|
1823
|
+
? undefined
|
|
1824
|
+
: self.rowNamesSet.get(relativeTo)
|
|
1825
|
+
},
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* #getter
|
|
1829
|
+
* row indices highlighted by the current tree hover (a hovered internal
|
|
1830
|
+
* node highlights every tip below it). Shared by the tree and MSA overlay
|
|
1831
|
+
* canvases so they cannot disagree, and resolved through the memoized
|
|
1832
|
+
* name->index map rather than rebuilding a lookup on each mouse move.
|
|
1833
|
+
*/
|
|
1834
|
+
get hoveredRowIndices() {
|
|
1835
|
+
const { hoveredTreeNode, rowNamesSet } = self
|
|
1836
|
+
return hoveredTreeNode
|
|
1837
|
+
? hoveredTreeNode.descendantNames
|
|
1838
|
+
.map(name => rowNamesSet.get(name))
|
|
1839
|
+
.filter(notEmpty)
|
|
1840
|
+
: []
|
|
1841
|
+
},
|
|
1842
|
+
|
|
1843
|
+
/**
|
|
1844
|
+
* #getter
|
|
1845
|
+
* contiguous runs of `highlightedColumns`, so a run of highlighted columns
|
|
1846
|
+
* draws as one bordered band. Computed here because the overlay canvas
|
|
1847
|
+
* redraws on every mouse move while the highlight itself rarely changes.
|
|
1848
|
+
*/
|
|
1849
|
+
get highlightedColumnRuns() {
|
|
1850
|
+
const { highlightedColumns } = self
|
|
1851
|
+
const runs: { start: number; end: number }[] = []
|
|
1852
|
+
for (const col of [...(highlightedColumns ?? [])].sort(
|
|
1853
|
+
(a, b) => a - b,
|
|
1854
|
+
)) {
|
|
1855
|
+
const last = runs.at(-1)
|
|
1856
|
+
if (last && col === last.end + 1) {
|
|
1857
|
+
last.end = col
|
|
1858
|
+
} else {
|
|
1859
|
+
runs.push({ start: col, end: col })
|
|
1860
|
+
}
|
|
1763
1861
|
}
|
|
1764
|
-
return
|
|
1862
|
+
return runs
|
|
1765
1863
|
},
|
|
1766
1864
|
|
|
1767
1865
|
/**
|
|
@@ -1775,14 +1873,14 @@ function stateModelFactory() {
|
|
|
1775
1873
|
if (mouseCol === undefined) {
|
|
1776
1874
|
return undefined
|
|
1777
1875
|
}
|
|
1778
|
-
const
|
|
1779
|
-
const total =
|
|
1780
|
-
if (
|
|
1876
|
+
const { colStats } = self
|
|
1877
|
+
const total = colStats.total(mouseCol)
|
|
1878
|
+
if (mouseCol >= colStats.numColumns || !total) {
|
|
1781
1879
|
return undefined
|
|
1782
1880
|
}
|
|
1783
|
-
const gaps = (
|
|
1784
|
-
const distribution =
|
|
1785
|
-
.
|
|
1881
|
+
const gaps = colStats.gapCount(mouseCol)
|
|
1882
|
+
const distribution = colStats
|
|
1883
|
+
.residueEntries(mouseCol)
|
|
1786
1884
|
.sort((a, b) => b[1] - a[1])
|
|
1787
1885
|
const consensus = distribution[0]
|
|
1788
1886
|
return {
|
|
@@ -1914,6 +2012,29 @@ function stateModelFactory() {
|
|
|
1914
2012
|
}),
|
|
1915
2013
|
)
|
|
1916
2014
|
|
|
2015
|
+
// track the live device pixel ratio so canvas backing stores re-scale
|
|
2016
|
+
// when the window moves between monitors or the browser zooms. The
|
|
2017
|
+
// matchMedia query is pinned to the current ratio, so each change
|
|
2018
|
+
// re-registers against the new one to keep tracking further moves.
|
|
2019
|
+
if (
|
|
2020
|
+
typeof window !== 'undefined' &&
|
|
2021
|
+
typeof window.matchMedia === 'function'
|
|
2022
|
+
) {
|
|
2023
|
+
const query = () =>
|
|
2024
|
+
window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
|
|
2025
|
+
let mql = query()
|
|
2026
|
+
const onChange = () => {
|
|
2027
|
+
self.setHighResScaleFactor(window.devicePixelRatio)
|
|
2028
|
+
mql.removeEventListener('change', onChange)
|
|
2029
|
+
mql = query()
|
|
2030
|
+
mql.addEventListener('change', onChange)
|
|
2031
|
+
}
|
|
2032
|
+
mql.addEventListener('change', onChange)
|
|
2033
|
+
addDisposer(self, () => {
|
|
2034
|
+
mql.removeEventListener('change', onChange)
|
|
2035
|
+
})
|
|
2036
|
+
}
|
|
2037
|
+
|
|
1917
2038
|
// autorun opens treeFilehandle. generation guard: if the filehandle
|
|
1918
2039
|
// changes mid-fetch, a slower earlier request must not clobber the data
|
|
1919
2040
|
// (or loading flag) set by the newer one
|
|
@@ -2084,18 +2205,32 @@ function stateModelFactory() {
|
|
|
2084
2205
|
|
|
2085
2206
|
// Keep the parse chain warm: reading self.columns transitively holds
|
|
2086
2207
|
// self.MSA (parseMSA) computed alive, so it is parsed once per data
|
|
2087
|
-
// change rather than re-parsed on every non-reactive access.
|
|
2088
|
-
//
|
|
2208
|
+
// change rather than re-parsed on every non-reactive access. Do not
|
|
2209
|
+
// remove.
|
|
2089
2210
|
// xref solution https://github.com/mobxjs/mobx/issues/266#issuecomment-222007278
|
|
2090
2211
|
// xref problem https://github.com/GMOD/react-msaview/issues/75
|
|
2212
|
+
//
|
|
2213
|
+
// The column statistics are held for the same reason whenever something
|
|
2214
|
+
// reads them off the reactive path: dynamic color schemes, and the hover
|
|
2215
|
+
// tooltip, which is read from a mousemove handler. Without this the
|
|
2216
|
+
// tooltip's cost depends on whether some visible track happens to be
|
|
2217
|
+
// observing them, so closing the conservation track would silently turn
|
|
2218
|
+
// every mouse move into a full-alignment recount.
|
|
2091
2219
|
addDisposer(
|
|
2092
2220
|
self,
|
|
2093
2221
|
autorun(() => {
|
|
2094
2222
|
if (self.colorSchemeName.includes('dynamic')) {
|
|
2095
2223
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
2096
2224
|
self.colStats
|
|
2225
|
+
}
|
|
2226
|
+
if (self.showColumnStats) {
|
|
2227
|
+
// everything the hover tooltip reads per column
|
|
2228
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
2229
|
+
self.colStats
|
|
2230
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
2231
|
+
self.conservation
|
|
2097
2232
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
2098
|
-
self.
|
|
2233
|
+
self.propertyConservation
|
|
2099
2234
|
}
|
|
2100
2235
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
2101
2236
|
self.columns
|
|
@@ -2134,7 +2269,7 @@ function stateModelFactory() {
|
|
|
2134
2269
|
},
|
|
2135
2270
|
}))
|
|
2136
2271
|
.postProcessSnapshot(({ data, ...rest }) => ({
|
|
2137
|
-
// per-property defaults are stripped by
|
|
2272
|
+
// per-property defaults are stripped by the stripDefault helper; the only thing
|
|
2138
2273
|
// it can't express is this cross-field rule: drop inline tree/msa/metadata
|
|
2139
2274
|
// when a sibling filehandle can refetch them, keeping sessions/URLs small
|
|
2140
2275
|
...rest,
|
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest'
|
|
2
2
|
|
|
3
|
+
import { columnCountsFromColumns } from './columnCounts.ts'
|
|
3
4
|
import { calculatePropertyConservation } from './propertyConservation.ts'
|
|
4
5
|
|
|
5
|
-
function counts(letters: string) {
|
|
6
|
-
const stats: Record<string, number> = {}
|
|
7
|
-
for (const c of letters) {
|
|
8
|
-
stats[c] = (stats[c] ?? 0) + 1
|
|
9
|
-
}
|
|
10
|
-
return stats
|
|
11
|
-
}
|
|
12
|
-
|
|
13
6
|
function propertyConservation(columns: string[]) {
|
|
14
|
-
|
|
15
|
-
const colStatsSums = colStats.map(s =>
|
|
16
|
-
Object.values(s).reduce((a, b) => a + b, 0),
|
|
17
|
-
)
|
|
18
|
-
return calculatePropertyConservation(colStats, colStatsSums)
|
|
7
|
+
return calculatePropertyConservation(columnCountsFromColumns(columns))
|
|
19
8
|
}
|
|
20
9
|
|
|
21
10
|
describe('property conservation', () => {
|