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/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,25 +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: types.
|
|
143
|
-
types.boolean,
|
|
144
|
-
defaultSubFeatureRows,
|
|
145
|
-
),
|
|
147
|
+
subFeatureRows: stripDefault(types.boolean, defaultSubFeatureRows),
|
|
146
148
|
|
|
147
149
|
/**
|
|
148
150
|
* #property
|
|
@@ -153,46 +155,43 @@ function stateModelFactory() {
|
|
|
153
155
|
/**
|
|
154
156
|
* #property
|
|
155
157
|
*/
|
|
156
|
-
drawMsaLetters: types.
|
|
157
|
-
types.boolean,
|
|
158
|
-
defaultDrawMsaLetters,
|
|
159
|
-
),
|
|
158
|
+
drawMsaLetters: stripDefault(types.boolean, defaultDrawMsaLetters),
|
|
160
159
|
|
|
161
160
|
/**
|
|
162
161
|
* #property
|
|
163
162
|
* zoom in/out on plain mouse-wheel without holding ctrl
|
|
164
163
|
*/
|
|
165
|
-
scrollZoom:
|
|
164
|
+
scrollZoom: stripDefault(types.boolean, defaultScrollZoom),
|
|
166
165
|
|
|
167
166
|
/**
|
|
168
167
|
* #property
|
|
169
168
|
* height of the div containing the view, px
|
|
170
169
|
*/
|
|
171
|
-
height:
|
|
170
|
+
height: stripDefault(types.number, defaultHeight),
|
|
172
171
|
|
|
173
172
|
/**
|
|
174
173
|
* #property
|
|
175
174
|
* height of each row, px
|
|
176
175
|
*/
|
|
177
|
-
rowHeight:
|
|
176
|
+
rowHeight: stripDefault(types.number, defaultRowHeight),
|
|
178
177
|
|
|
179
178
|
/**
|
|
180
179
|
* #property
|
|
181
180
|
* scroll position, Y-offset, px
|
|
182
181
|
*/
|
|
183
|
-
scrollY:
|
|
182
|
+
scrollY: stripDefault(types.number, defaultScrollY),
|
|
184
183
|
|
|
185
184
|
/**
|
|
186
185
|
* #property
|
|
187
186
|
* scroll position, X-offset, px
|
|
188
187
|
*/
|
|
189
|
-
scrollX:
|
|
188
|
+
scrollX: stripDefault(types.number, defaultScrollX),
|
|
190
189
|
|
|
191
190
|
/**
|
|
192
191
|
* #property
|
|
193
192
|
* width of columns, px
|
|
194
193
|
*/
|
|
195
|
-
colWidth:
|
|
194
|
+
colWidth: stripDefault(types.number, defaultColWidth),
|
|
196
195
|
|
|
197
196
|
/**
|
|
198
197
|
* #property
|
|
@@ -222,17 +221,14 @@ function stateModelFactory() {
|
|
|
222
221
|
/**
|
|
223
222
|
* #property
|
|
224
223
|
*/
|
|
225
|
-
currentAlignment: types.
|
|
226
|
-
types.number,
|
|
227
|
-
defaultCurrentAlignment,
|
|
228
|
-
),
|
|
224
|
+
currentAlignment: stripDefault(types.number, defaultCurrentAlignment),
|
|
229
225
|
|
|
230
226
|
/**
|
|
231
227
|
* #property
|
|
232
228
|
* array of tree parent nodes that are 'collapsed' (all children are
|
|
233
229
|
* hidden)
|
|
234
230
|
*/
|
|
235
|
-
collapsed:
|
|
231
|
+
collapsed: stripDefault(types.array(types.string), []),
|
|
236
232
|
|
|
237
233
|
/**
|
|
238
234
|
* #property
|
|
@@ -243,7 +239,7 @@ function stateModelFactory() {
|
|
|
243
239
|
* #property
|
|
244
240
|
* turned off tracks
|
|
245
241
|
*/
|
|
246
|
-
turnedOffTracks:
|
|
242
|
+
turnedOffTracks: stripDefault(types.map(types.boolean), {}),
|
|
247
243
|
|
|
248
244
|
/**
|
|
249
245
|
* #property
|
|
@@ -259,7 +255,7 @@ function stateModelFactory() {
|
|
|
259
255
|
/**
|
|
260
256
|
* #property
|
|
261
257
|
*/
|
|
262
|
-
featureFilters:
|
|
258
|
+
featureFilters: stripDefault(types.map(types.boolean), {}),
|
|
263
259
|
/**
|
|
264
260
|
* #property
|
|
265
261
|
*/
|
|
@@ -826,15 +822,19 @@ function stateModelFactory() {
|
|
|
826
822
|
if (strs.length === 0) {
|
|
827
823
|
return []
|
|
828
824
|
}
|
|
829
|
-
|
|
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)
|
|
830
829
|
const numRows = strs.length
|
|
831
830
|
const threshold = Math.ceil((realAllowedGappyness / 100) * numRows)
|
|
832
|
-
const blankCounts = new
|
|
831
|
+
const blankCounts = new Uint32Array(numCols)
|
|
833
832
|
for (let j = 0; j < numRows; j++) {
|
|
834
833
|
const str = strs[j]!
|
|
834
|
+
const len = str.length
|
|
835
835
|
for (let i = 0; i < numCols; i++) {
|
|
836
836
|
// bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
|
|
837
|
-
if ((str.charCodeAt(i) - 45) >>> 0 <= 1) {
|
|
837
|
+
if (i >= len || (str.charCodeAt(i) - 45) >>> 0 <= 1) {
|
|
838
838
|
blankCounts[i]!++
|
|
839
839
|
}
|
|
840
840
|
}
|
|
@@ -882,9 +882,27 @@ function stateModelFactory() {
|
|
|
882
882
|
},
|
|
883
883
|
/**
|
|
884
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.
|
|
885
889
|
*/
|
|
886
890
|
get numRows() {
|
|
887
|
-
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
|
+
)
|
|
888
906
|
},
|
|
889
907
|
|
|
890
908
|
/**
|
|
@@ -924,23 +942,15 @@ function stateModelFactory() {
|
|
|
924
942
|
* #getter
|
|
925
943
|
*/
|
|
926
944
|
get colStats() {
|
|
927
|
-
|
|
928
|
-
const columns = this.columns2d
|
|
929
|
-
for (const column of columns) {
|
|
930
|
-
for (let j = 0; j < column.length; j++) {
|
|
931
|
-
const l = (r[j] ??= {})
|
|
932
|
-
const cj = column[j]!
|
|
933
|
-
l[cj] = (l[cj] ?? 0) + 1
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
return r
|
|
945
|
+
return columnCountsFromRows(this.columns2d)
|
|
937
946
|
},
|
|
938
947
|
|
|
939
948
|
/**
|
|
940
949
|
* #getter
|
|
950
|
+
* per-column residue totals (gaps included)
|
|
941
951
|
*/
|
|
942
952
|
get colStatsSums() {
|
|
943
|
-
return this.colStats.
|
|
953
|
+
return this.colStats.totals
|
|
944
954
|
},
|
|
945
955
|
|
|
946
956
|
/**
|
|
@@ -949,14 +959,7 @@ function stateModelFactory() {
|
|
|
949
959
|
* Returns 'dna', 'rna', or 'amino'.
|
|
950
960
|
*/
|
|
951
961
|
get sequenceType(): 'dna' | 'rna' | 'amino' {
|
|
952
|
-
const letters =
|
|
953
|
-
for (const stats of this.colStats) {
|
|
954
|
-
for (const letter of Object.keys(stats)) {
|
|
955
|
-
if (letter !== '-' && letter !== '.') {
|
|
956
|
-
letters.add(letter)
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
}
|
|
962
|
+
const letters = this.colStats.lettersPresent
|
|
960
963
|
// isDna already excludes U (not in the DNA set) and isRna excludes T,
|
|
961
964
|
// so the set membership alone disambiguates the two
|
|
962
965
|
const dna = new Set(['A', 'C', 'G', 'T', 'N'])
|
|
@@ -972,21 +975,18 @@ function stateModelFactory() {
|
|
|
972
975
|
* Used by percent_identity_dynamic color scheme.
|
|
973
976
|
*/
|
|
974
977
|
get colConsensus() {
|
|
975
|
-
const { colStats
|
|
976
|
-
return colStats.
|
|
977
|
-
const total =
|
|
978
|
-
if (!total) {
|
|
979
|
-
return { letter: '', color: undefined }
|
|
980
|
-
}
|
|
978
|
+
const { colStats } = this
|
|
979
|
+
return Array.from({ length: colStats.numColumns }, (_, col) => {
|
|
980
|
+
const total = colStats.total(col)
|
|
981
981
|
let maxCount = 0
|
|
982
982
|
let letter = ''
|
|
983
|
-
|
|
984
|
-
if (
|
|
985
|
-
maxCount =
|
|
986
|
-
letter =
|
|
983
|
+
colStats.forEachResidue(col, (slot, count) => {
|
|
984
|
+
if (count > maxCount) {
|
|
985
|
+
maxCount = count
|
|
986
|
+
letter = letterOfResidueSlot(slot)
|
|
987
987
|
}
|
|
988
|
-
}
|
|
989
|
-
const proportion = maxCount / total
|
|
988
|
+
})
|
|
989
|
+
const proportion = total ? maxCount / total : 0
|
|
990
990
|
return {
|
|
991
991
|
letter,
|
|
992
992
|
color:
|
|
@@ -1004,9 +1004,9 @@ function stateModelFactory() {
|
|
|
1004
1004
|
* ref http://www.jalview.org/help/html/colourSchemes/clustal.html
|
|
1005
1005
|
*/
|
|
1006
1006
|
get colClustalX() {
|
|
1007
|
-
const { colStats
|
|
1008
|
-
return colStats.
|
|
1009
|
-
clustalXColumnColors(
|
|
1007
|
+
const { colStats } = this
|
|
1008
|
+
return Array.from({ length: colStats.numColumns }, (_, col) =>
|
|
1009
|
+
clustalXColumnColors(colStats, col),
|
|
1010
1010
|
)
|
|
1011
1011
|
},
|
|
1012
1012
|
|
|
@@ -1017,33 +1017,25 @@ function stateModelFactory() {
|
|
|
1017
1017
|
* Returns values 0-1 where 1 = fully conserved, 0 = no conservation.
|
|
1018
1018
|
*/
|
|
1019
1019
|
get conservation() {
|
|
1020
|
-
const { colStats,
|
|
1020
|
+
const { colStats, sequenceType } = this
|
|
1021
1021
|
const alphabetSize = sequenceType === 'amino' ? 20 : 4
|
|
1022
1022
|
const maxEntropy = Math.log2(alphabetSize)
|
|
1023
1023
|
|
|
1024
|
-
return colStats.
|
|
1025
|
-
const total =
|
|
1026
|
-
|
|
1027
|
-
return 0
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
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)
|
|
1031
1027
|
const nonGapTotal = total - gapCount
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
let entropy = 0
|
|
1037
|
-
for (const [letter, count] of Object.entries(stats)) {
|
|
1038
|
-
if (letter !== '-' && letter !== '.') {
|
|
1028
|
+
let score = 0
|
|
1029
|
+
if (nonGapTotal > 0) {
|
|
1030
|
+
let entropy = 0
|
|
1031
|
+
colStats.forEachResidue(col, (_slot, count) => {
|
|
1039
1032
|
const freq = count / nonGapTotal
|
|
1040
1033
|
entropy -= freq * Math.log2(freq)
|
|
1041
|
-
}
|
|
1034
|
+
})
|
|
1035
|
+
score =
|
|
1036
|
+
Math.max(0, 1 - entropy / maxEntropy) * (1 - gapCount / total)
|
|
1042
1037
|
}
|
|
1043
|
-
|
|
1044
|
-
const gapFraction = gapCount / total
|
|
1045
|
-
const conservation = Math.max(0, 1 - entropy / maxEntropy)
|
|
1046
|
-
return conservation * (1 - gapFraction)
|
|
1038
|
+
return score
|
|
1047
1039
|
})
|
|
1048
1040
|
},
|
|
1049
1041
|
/**
|
|
@@ -1054,7 +1046,7 @@ function stateModelFactory() {
|
|
|
1054
1046
|
*/
|
|
1055
1047
|
get propertyConservation() {
|
|
1056
1048
|
return this.sequenceType === 'amino'
|
|
1057
|
-
? calculatePropertyConservation(this.colStats
|
|
1049
|
+
? calculatePropertyConservation(this.colStats)
|
|
1058
1050
|
: []
|
|
1059
1051
|
},
|
|
1060
1052
|
/**
|
|
@@ -1590,7 +1582,13 @@ function stateModelFactory() {
|
|
|
1590
1582
|
*/
|
|
1591
1583
|
seqPosToGlobalCol(rowName: string, seqPos: number) {
|
|
1592
1584
|
const seq = self.rowMap.get(rowName)
|
|
1593
|
-
|
|
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)
|
|
1594
1592
|
},
|
|
1595
1593
|
|
|
1596
1594
|
/**
|
|
@@ -1746,6 +1744,60 @@ function stateModelFactory() {
|
|
|
1746
1744
|
.toSorted((a, b) => a.start - b.start)
|
|
1747
1745
|
},
|
|
1748
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
|
+
|
|
1749
1801
|
/**
|
|
1750
1802
|
* #getter
|
|
1751
1803
|
* domain annotations under the mouse, hit-tested against the exact visible
|
|
@@ -1754,20 +1806,60 @@ function stateModelFactory() {
|
|
|
1754
1806
|
get mouseOverDomains() {
|
|
1755
1807
|
const { mouseCol } = self
|
|
1756
1808
|
const name = self.mouseOverRowName
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
+
}
|
|
1769
1861
|
}
|
|
1770
|
-
return
|
|
1862
|
+
return runs
|
|
1771
1863
|
},
|
|
1772
1864
|
|
|
1773
1865
|
/**
|
|
@@ -1781,14 +1873,14 @@ function stateModelFactory() {
|
|
|
1781
1873
|
if (mouseCol === undefined) {
|
|
1782
1874
|
return undefined
|
|
1783
1875
|
}
|
|
1784
|
-
const
|
|
1785
|
-
const total =
|
|
1786
|
-
if (
|
|
1876
|
+
const { colStats } = self
|
|
1877
|
+
const total = colStats.total(mouseCol)
|
|
1878
|
+
if (mouseCol >= colStats.numColumns || !total) {
|
|
1787
1879
|
return undefined
|
|
1788
1880
|
}
|
|
1789
|
-
const gaps = (
|
|
1790
|
-
const distribution =
|
|
1791
|
-
.
|
|
1881
|
+
const gaps = colStats.gapCount(mouseCol)
|
|
1882
|
+
const distribution = colStats
|
|
1883
|
+
.residueEntries(mouseCol)
|
|
1792
1884
|
.sort((a, b) => b[1] - a[1])
|
|
1793
1885
|
const consensus = distribution[0]
|
|
1794
1886
|
return {
|
|
@@ -1920,6 +2012,29 @@ function stateModelFactory() {
|
|
|
1920
2012
|
}),
|
|
1921
2013
|
)
|
|
1922
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
|
+
|
|
1923
2038
|
// autorun opens treeFilehandle. generation guard: if the filehandle
|
|
1924
2039
|
// changes mid-fetch, a slower earlier request must not clobber the data
|
|
1925
2040
|
// (or loading flag) set by the newer one
|
|
@@ -2090,18 +2205,32 @@ function stateModelFactory() {
|
|
|
2090
2205
|
|
|
2091
2206
|
// Keep the parse chain warm: reading self.columns transitively holds
|
|
2092
2207
|
// self.MSA (parseMSA) computed alive, so it is parsed once per data
|
|
2093
|
-
// change rather than re-parsed on every non-reactive access.
|
|
2094
|
-
//
|
|
2208
|
+
// change rather than re-parsed on every non-reactive access. Do not
|
|
2209
|
+
// remove.
|
|
2095
2210
|
// xref solution https://github.com/mobxjs/mobx/issues/266#issuecomment-222007278
|
|
2096
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.
|
|
2097
2219
|
addDisposer(
|
|
2098
2220
|
self,
|
|
2099
2221
|
autorun(() => {
|
|
2100
2222
|
if (self.colorSchemeName.includes('dynamic')) {
|
|
2101
2223
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
2102
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
|
|
2103
2232
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
2104
|
-
self.
|
|
2233
|
+
self.propertyConservation
|
|
2105
2234
|
}
|
|
2106
2235
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
2107
2236
|
self.columns
|
|
@@ -2140,7 +2269,7 @@ function stateModelFactory() {
|
|
|
2140
2269
|
},
|
|
2141
2270
|
}))
|
|
2142
2271
|
.postProcessSnapshot(({ data, ...rest }) => ({
|
|
2143
|
-
// per-property defaults are stripped by
|
|
2272
|
+
// per-property defaults are stripped by the stripDefault helper; the only thing
|
|
2144
2273
|
// it can't express is this cross-field rule: drop inline tree/msa/metadata
|
|
2145
2274
|
// when a sibling filehandle can refetch them, keeping sessions/URLs small
|
|
2146
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', () => {
|