react-msaview 5.9.0 → 6.0.0
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 +99 -99
- package/bundle/index.js.map +4 -4
- package/dist/clustalX.js +22 -22
- package/dist/clustalX.js.map +1 -1
- package/dist/components/ConservationTrack.js +21 -16
- package/dist/components/ConservationTrack.js.map +1 -1
- package/dist/components/CopyButton.d.ts +9 -0
- package/dist/components/CopyButton.js +23 -0
- package/dist/components/CopyButton.js.map +1 -0
- package/dist/components/Loading.js +2 -2
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAView.js +18 -24
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/ResizeHandles.js +24 -40
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/ScrollbarThumb.d.ts +14 -0
- package/dist/components/ScrollbarThumb.js +28 -0
- package/dist/components/ScrollbarThumb.js.map +1 -0
- package/dist/components/SequenceTextArea.js +3 -10
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/TextTrack.js +26 -21
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +11 -25
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/FeatureDialog.js +3 -3
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/dialogs/{UserProvidedDomainsDialog.d.ts → InterProScanFileDialog.d.ts} +2 -2
- package/dist/components/dialogs/{UserProvidedDomainsDialog.js → InterProScanFileDialog.js} +6 -10
- package/dist/components/dialogs/InterProScanFileDialog.js.map +1 -0
- package/dist/components/dialogs/TrackInfoDialog.d.ts +2 -4
- package/dist/components/dialogs/TrackInfoDialog.js +8 -14
- package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
- package/dist/components/header/FileMenu.js +3 -3
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/GappynessSlider.js +1 -1
- package/dist/components/header/GappynessSlider.js.map +1 -1
- package/dist/components/header/{getDomainsMenu.d.ts → getAnnotationsMenu.d.ts} +1 -1
- package/dist/components/header/{getDomainsMenu.js → getAnnotationsMenu.js} +10 -9
- package/dist/components/header/getAnnotationsMenu.js.map +1 -0
- package/dist/components/header/settingsMenuItems.js +39 -72
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/import/ImportForm.js +1 -1
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/import/ImportFormExamples.js +66 -57
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/minimap/Minimap.js +11 -22
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/minimap/MinimapSVG.d.ts +2 -1
- package/dist/components/minimap/MinimapSVG.js +3 -4
- package/dist/components/minimap/MinimapSVG.js.map +1 -1
- package/dist/components/minimap/minimapLayout.d.ts +10 -1
- package/dist/components/minimap/minimapLayout.js +15 -10
- package/dist/components/minimap/minimapLayout.js.map +1 -1
- package/dist/components/msa/{DomainLegend.d.ts → AnnotationLegend.d.ts} +2 -2
- package/dist/components/msa/{DomainLegend.js → AnnotationLegend.js} +4 -4
- package/dist/components/msa/AnnotationLegend.js.map +1 -0
- package/dist/components/msa/MSACanvas.js +2 -2
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +19 -16
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +14 -8
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/MSAPanel.js +2 -2
- package/dist/components/msa/MSAPanel.js.map +1 -1
- package/dist/components/msa/domainBandCursor.d.ts +17 -0
- package/dist/components/msa/domainBandCursor.js +39 -0
- package/dist/components/msa/domainBandCursor.js.map +1 -0
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +9 -6
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +14 -20
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +32 -33
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/overlayColors.d.ts +2 -0
- package/dist/components/overlayColors.js +5 -0
- package/dist/components/overlayColors.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +29 -24
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +19 -12
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeRuler.js +5 -2
- package/dist/components/tree/TreeRuler.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +0 -38
- package/dist/components/tree/renderTreeCanvas.js +17 -17
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/ggplotPalettes.d.ts +0 -1
- package/dist/ggplotPalettes.js +0 -3
- package/dist/ggplotPalettes.js.map +1 -1
- package/dist/hierarchy.d.ts +2 -6
- package/dist/hierarchy.js +4 -6
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/launchInterProScan.d.ts +0 -23
- package/dist/launchInterProScan.js +2 -5
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/measureTextCanvas.js +21 -6
- package/dist/measureTextCanvas.js.map +1 -1
- package/dist/model.d.ts +50 -100
- package/dist/model.js +209 -219
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +12 -19
- package/dist/neighborJoining.js.map +1 -1
- package/dist/parseAsn1.js +32 -43
- package/dist/parseAsn1.js.map +1 -1
- package/dist/renderToSvg.js +5 -2
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rowCoordinateCalculations.d.ts +13 -17
- package/dist/rowCoordinateCalculations.js +45 -39
- package/dist/rowCoordinateCalculations.js.map +1 -1
- package/dist/seqPosToGlobalCol.d.ts +6 -19
- package/dist/seqPosToGlobalCol.js +6 -33
- package/dist/seqPosToGlobalCol.js.map +1 -1
- package/dist/types.d.ts +4 -11
- package/dist/useCanvasAutorun.d.ts +23 -1
- package/dist/useCanvasAutorun.js +20 -8
- package/dist/useCanvasAutorun.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
- package/src/clustalX.ts +22 -22
- package/src/colSpace.test.ts +27 -0
- package/src/components/ConservationTrack.tsx +7 -5
- package/src/components/CopyButton.tsx +44 -0
- package/src/components/Loading.tsx +10 -1
- package/src/components/MSAView.tsx +25 -32
- package/src/components/ResizeHandles.tsx +60 -69
- package/src/components/ScrollbarThumb.tsx +48 -0
- package/src/components/SequenceTextArea.tsx +3 -16
- package/src/components/TextTrack.tsx +7 -12
- package/src/components/VerticalScrollbar.tsx +19 -33
- package/src/components/dialogs/FeatureDialog.tsx +3 -4
- package/src/components/dialogs/{UserProvidedDomainsDialog.tsx → InterProScanFileDialog.tsx} +6 -14
- package/src/components/dialogs/TrackInfoDialog.tsx +18 -20
- package/src/components/header/FileMenu.tsx +3 -3
- package/src/components/header/GappynessSlider.tsx +4 -1
- package/src/components/header/{getDomainsMenu.ts → getAnnotationsMenu.ts} +10 -9
- package/src/components/header/settingsMenuItems.ts +40 -72
- package/src/components/import/ImportForm.tsx +1 -1
- package/src/components/import/ImportFormExamples.tsx +82 -133
- package/src/components/minimap/Minimap.tsx +23 -31
- package/src/components/minimap/MinimapSVG.tsx +32 -31
- package/src/components/minimap/minimapLayout.test.ts +60 -0
- package/src/components/minimap/minimapLayout.ts +15 -10
- package/src/components/msa/{DomainLegend.tsx → AnnotationLegend.tsx} +7 -3
- package/src/components/msa/MSACanvas.tsx +3 -10
- package/src/components/msa/MSACanvasBlock.tsx +16 -21
- package/src/components/msa/MSAMouseoverCanvas.tsx +11 -9
- package/src/components/msa/MSAPanel.tsx +2 -2
- package/src/components/msa/domainBandCursor.ts +41 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +51 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +13 -8
- package/src/components/msa/renderMSABlock.ts +19 -23
- package/src/components/msa/renderMSAMouseover.ts +38 -35
- package/src/components/overlayColors.ts +6 -0
- package/src/components/tree/TreeCanvas.tsx +6 -4
- package/src/components/tree/TreeCanvasBlock.tsx +11 -12
- package/src/components/tree/TreeRuler.tsx +7 -2
- package/src/components/tree/renderTreeCanvas.test.ts +15 -7
- package/src/components/tree/renderTreeCanvas.ts +20 -17
- package/src/domainBands.test.ts +24 -0
- package/src/domainLetterColor.test.ts +80 -0
- package/src/ggplotPalettes.ts +0 -4
- package/src/hierarchy.test.ts +7 -7
- package/src/hierarchy.ts +6 -9
- package/src/index.ts +4 -1
- package/src/largeTree.test.ts +23 -2
- package/src/launchInterProScan.ts +3 -30
- package/src/loadErrorDisplay.test.tsx +102 -0
- package/src/measureTextCanvas.ts +22 -6
- package/src/model.ts +233 -244
- package/src/modelFilehandleLoaders.test.ts +137 -0
- package/src/neighborJoining.test.ts +12 -0
- package/src/neighborJoining.ts +12 -20
- package/src/parseAsn1.ts +32 -40
- package/src/{renderDomainLegendSvg.test.tsx → renderDomainsSvg.test.tsx} +44 -20
- package/src/renderMinimapSvg.test.tsx +109 -0
- package/src/renderToSvg.tsx +5 -2
- package/src/rowCoordinateCalculations.ts +48 -44
- package/src/seqPosToGlobalCol.test.ts +39 -51
- package/src/seqPosToGlobalCol.ts +6 -41
- package/src/types.ts +10 -15
- package/src/useCanvasAutorun.test.tsx +98 -0
- package/src/useCanvasAutorun.ts +30 -11
- package/src/version.ts +1 -1
- package/src/viewChrome.test.tsx +134 -0
- package/dist/components/dialogs/DomainDialog.d.ts +0 -6
- package/dist/components/dialogs/DomainDialog.js +0 -19
- package/dist/components/dialogs/DomainDialog.js.map +0 -1
- package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +0 -1
- package/dist/components/header/SettingsMenu.d.ts +0 -6
- package/dist/components/header/SettingsMenu.js +0 -40
- package/dist/components/header/SettingsMenu.js.map +0 -1
- package/dist/components/header/getDomainsMenu.js.map +0 -1
- package/dist/components/msa/DomainLegend.js.map +0 -1
- package/src/components/dialogs/DomainDialog.tsx +0 -45
- package/src/components/header/SettingsMenu.tsx +0 -57
|
@@ -45,7 +45,7 @@ export function getNodeX(
|
|
|
45
45
|
return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * tipX
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
function renderTree({
|
|
49
49
|
offsetY,
|
|
50
50
|
ctx,
|
|
51
51
|
model,
|
|
@@ -89,7 +89,7 @@ export function renderTree({
|
|
|
89
89
|
})
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
function renderCollapsedTriangles({
|
|
93
93
|
ctx,
|
|
94
94
|
clickMap,
|
|
95
95
|
offsetY,
|
|
@@ -98,6 +98,7 @@ export function renderCollapsedTriangles({
|
|
|
98
98
|
tipX,
|
|
99
99
|
maxDepthToLeaf,
|
|
100
100
|
blockSizeYOverride,
|
|
101
|
+
collapsedSet,
|
|
101
102
|
}: {
|
|
102
103
|
ctx: RenderCtx
|
|
103
104
|
clickMap?: ClickMapIndex
|
|
@@ -107,11 +108,11 @@ export function renderCollapsedTriangles({
|
|
|
107
108
|
tipX: number
|
|
108
109
|
maxDepthToLeaf: number
|
|
109
110
|
blockSizeYOverride?: number
|
|
111
|
+
collapsedSet: Set<string>
|
|
110
112
|
}) {
|
|
111
113
|
const {
|
|
112
114
|
hierarchy,
|
|
113
115
|
showBranchLenEffective: showBranchLen,
|
|
114
|
-
collapsed,
|
|
115
116
|
blockSize,
|
|
116
117
|
rowHeight,
|
|
117
118
|
fontSize,
|
|
@@ -119,10 +120,9 @@ export function renderCollapsedTriangles({
|
|
|
119
120
|
} = model
|
|
120
121
|
// nothing collapsed is the common case, and the traversal below visits every
|
|
121
122
|
// node in the tree on every block of every redraw
|
|
122
|
-
if (
|
|
123
|
+
if (collapsedSet.size === 0) {
|
|
123
124
|
return
|
|
124
125
|
}
|
|
125
|
-
const collapsedSet = new Set(collapsed)
|
|
126
126
|
const by = blockSizeYOverride ?? blockSize
|
|
127
127
|
const halfHeight = Math.max(2, rowHeight * 0.42)
|
|
128
128
|
forEachDescendant(hierarchy, node => {
|
|
@@ -171,7 +171,7 @@ export function renderCollapsedTriangles({
|
|
|
171
171
|
})
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
function renderNodeBubbles({
|
|
175
175
|
ctx,
|
|
176
176
|
clickMap,
|
|
177
177
|
offsetY,
|
|
@@ -179,6 +179,7 @@ export function renderNodeBubbles({
|
|
|
179
179
|
tipX,
|
|
180
180
|
maxDepthToLeaf,
|
|
181
181
|
blockSizeYOverride,
|
|
182
|
+
collapsedSet,
|
|
182
183
|
}: {
|
|
183
184
|
ctx: RenderCtx
|
|
184
185
|
clickMap?: ClickMapIndex
|
|
@@ -187,16 +188,15 @@ export function renderNodeBubbles({
|
|
|
187
188
|
tipX: number
|
|
188
189
|
maxDepthToLeaf: number
|
|
189
190
|
blockSizeYOverride?: number
|
|
191
|
+
collapsedSet: Set<string>
|
|
190
192
|
}) {
|
|
191
193
|
const {
|
|
192
194
|
hierarchy,
|
|
193
195
|
showBranchLenEffective: showBranchLen,
|
|
194
|
-
collapsed,
|
|
195
196
|
blockSize,
|
|
196
197
|
marginLeft: ml,
|
|
197
198
|
} = model
|
|
198
199
|
const by = blockSizeYOverride ?? blockSize
|
|
199
|
-
const collapsedSet = new Set(collapsed)
|
|
200
200
|
forEachDescendant(hierarchy, node => {
|
|
201
201
|
const x = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf)
|
|
202
202
|
if (x === undefined) {
|
|
@@ -231,7 +231,7 @@ export function renderNodeBubbles({
|
|
|
231
231
|
})
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
function renderTreeLabels({
|
|
235
235
|
theme,
|
|
236
236
|
model,
|
|
237
237
|
offsetY,
|
|
@@ -240,6 +240,7 @@ export function renderTreeLabels({
|
|
|
240
240
|
tipX,
|
|
241
241
|
maxDepthToLeaf,
|
|
242
242
|
blockSizeYOverride,
|
|
243
|
+
collapsedSet,
|
|
243
244
|
}: {
|
|
244
245
|
model: MsaViewModel
|
|
245
246
|
offsetY: number
|
|
@@ -249,6 +250,7 @@ export function renderTreeLabels({
|
|
|
249
250
|
tipX: number
|
|
250
251
|
maxDepthToLeaf: number
|
|
251
252
|
blockSizeYOverride?: number
|
|
253
|
+
collapsedSet: Set<string>
|
|
252
254
|
}) {
|
|
253
255
|
const {
|
|
254
256
|
fontSize,
|
|
@@ -262,16 +264,11 @@ export function renderTreeLabels({
|
|
|
262
264
|
marginLeft,
|
|
263
265
|
noTree,
|
|
264
266
|
labelWidthMap,
|
|
265
|
-
collapsed,
|
|
266
267
|
} = model
|
|
268
|
+
const by = blockSizeYOverride ?? blockSize
|
|
267
269
|
// labels only exist for leaves, which are laid out top to bottom, so take the
|
|
268
270
|
// same slice the MSA renderer uses instead of walking every tip per block
|
|
269
|
-
const visibleLeaves = getVisibleLeaves({
|
|
270
|
-
model,
|
|
271
|
-
offsetY,
|
|
272
|
-
blockSizeY: blockSizeYOverride ?? model.blockSize,
|
|
273
|
-
})
|
|
274
|
-
const by = blockSizeYOverride ?? blockSize
|
|
271
|
+
const visibleLeaves = getVisibleLeaves({ model, offsetY, blockSizeY: by })
|
|
275
272
|
const emHeight = ctx.measureText('M').width
|
|
276
273
|
if (labelsAlignRight) {
|
|
277
274
|
ctx.textAlign = 'right'
|
|
@@ -289,7 +286,7 @@ export function renderTreeLabels({
|
|
|
289
286
|
const displayName = treeMetadata[name]?.genome || name
|
|
290
287
|
// a collapsed clade is drawn as a triangle + tip count; suppress its leaf
|
|
291
288
|
// label when the "name" is just the auto-generated internal-node id
|
|
292
|
-
const isAnonymousCollapsed =
|
|
289
|
+
const isAnonymousCollapsed = collapsedSet.has(id) && name === id
|
|
293
290
|
if (!isAnonymousCollapsed && inYBlock(y, offsetY, by)) {
|
|
294
291
|
// note: +rowHeight/4 matches with -rowHeight/4 in msa
|
|
295
292
|
const yp = y + fontSize / 4
|
|
@@ -389,6 +386,9 @@ export function renderTreeCanvas({
|
|
|
389
386
|
// maxBranchLength for the cladogram would pin every node to x=0 for a tree
|
|
390
387
|
// with no branch lengths -- which is exactly what forces cladogram mode.
|
|
391
388
|
const tipX = showBranchLenEffective ? maxBranchLength : treeWidth
|
|
389
|
+
// built once for all three passes: each of them asks about a different node on
|
|
390
|
+
// every block of every redraw, and `collapsed` is a list
|
|
391
|
+
const collapsedSet = new Set(model.collapsed)
|
|
392
392
|
|
|
393
393
|
if (!noTree && drawTree) {
|
|
394
394
|
renderTree({
|
|
@@ -410,6 +410,7 @@ export function renderTreeCanvas({
|
|
|
410
410
|
tipX,
|
|
411
411
|
maxDepthToLeaf,
|
|
412
412
|
blockSizeYOverride,
|
|
413
|
+
collapsedSet,
|
|
413
414
|
})
|
|
414
415
|
|
|
415
416
|
if (drawNodeBubbles) {
|
|
@@ -421,6 +422,7 @@ export function renderTreeCanvas({
|
|
|
421
422
|
tipX,
|
|
422
423
|
maxDepthToLeaf,
|
|
423
424
|
blockSizeYOverride,
|
|
425
|
+
collapsedSet,
|
|
424
426
|
})
|
|
425
427
|
}
|
|
426
428
|
}
|
|
@@ -435,6 +437,7 @@ export function renderTreeCanvas({
|
|
|
435
437
|
tipX,
|
|
436
438
|
maxDepthToLeaf,
|
|
437
439
|
blockSizeYOverride,
|
|
440
|
+
collapsedSet,
|
|
438
441
|
})
|
|
439
442
|
}
|
|
440
443
|
|
package/src/domainBands.test.ts
CHANGED
|
@@ -18,6 +18,12 @@ const gff = `##gff-version 3
|
|
|
18
18
|
a Pfam protein_match 1 4 . . . Name=PF00001;signature_desc=First
|
|
19
19
|
a Pfam protein_match 5 8 . . . Name=PF00002;signature_desc=Second`
|
|
20
20
|
|
|
21
|
+
// row `b` only: PF00003 ends inside the insertion, PF00004 lies entirely within
|
|
22
|
+
// it, so gap hiding puts one end and then both ends into hidden columns
|
|
23
|
+
const insertionGff = `##gff-version 3
|
|
24
|
+
b Pfam protein_match 4 8 . . . Name=PF00003;signature_desc=Overlapping
|
|
25
|
+
b Pfam protein_match 5 8 . . . Name=PF00004;signature_desc=Inside`
|
|
26
|
+
|
|
21
27
|
function makeModel() {
|
|
22
28
|
const model = MSAModelF().create({
|
|
23
29
|
id: 'domain-bands-test',
|
|
@@ -56,6 +62,24 @@ test('bands follow the columns when gappy columns are hidden', () => {
|
|
|
56
62
|
])
|
|
57
63
|
})
|
|
58
64
|
|
|
65
|
+
test('a band whose end lands in a hidden column keeps its visible part', () => {
|
|
66
|
+
const model = makeModel()
|
|
67
|
+
model.applyGFFText(insertionGff)
|
|
68
|
+
expect(model.domainBands.get('b')!.map(b => [b.startCol, b.endCol])).toEqual([
|
|
69
|
+
[3, 8],
|
|
70
|
+
[4, 8],
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
model.setHideGaps(true)
|
|
74
|
+
model.setAllowedGappyness(50)
|
|
75
|
+
expect(model.blanks).toEqual([4, 5, 6, 7])
|
|
76
|
+
// PF00003 covers V + the 4 hidden Qs, so only the V column survives; PF00004
|
|
77
|
+
// is nothing but hidden columns and drops out entirely
|
|
78
|
+
expect(model.domainBands.get('b')!.map(b => [b.startCol, b.endCol])).toEqual([
|
|
79
|
+
[3, 4],
|
|
80
|
+
])
|
|
81
|
+
})
|
|
82
|
+
|
|
59
83
|
test('mouseOverDomains hit-tests against the drawn span', () => {
|
|
60
84
|
const model = makeModel()
|
|
61
85
|
const rowIndex = model.rowNames.indexOf('a')
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// Letters drawn on top of the domain overlay take their color from the box
|
|
4
|
+
// underneath them. The overlay paints bands largest-first, so with nested
|
|
5
|
+
// signatures -- routine in InterProScan output, where a family encloses a
|
|
6
|
+
// domain -- the box actually visible at a column is the *last* one painted,
|
|
7
|
+
// not the first one covering it.
|
|
8
|
+
import { expect, test } from 'vitest'
|
|
9
|
+
|
|
10
|
+
import { domainBandCursor } from './components/msa/domainBandCursor.ts'
|
|
11
|
+
import MSAModelF from './model.ts'
|
|
12
|
+
|
|
13
|
+
const row = 'ACDEFGHIKLMNPQRSTVWY'
|
|
14
|
+
const msa = `>seq1\n${row}\n>seq2\n${row}`
|
|
15
|
+
|
|
16
|
+
// PF_OUTER spans the whole row, PF_INNER sits inside it. tidyInterProAnnotations
|
|
17
|
+
// orders by length descending, so the outer band paints first and the inner one
|
|
18
|
+
// covers it.
|
|
19
|
+
const gff = `##gff-version 3
|
|
20
|
+
seq1\tPfam\tprotein_match\t1\t20\t.\t.\t.\tName=PF_OUTER;signature_desc=Outer
|
|
21
|
+
seq1\tPfam\tprotein_match\t8\t12\t.\t.\t.\tName=PF_INNER;signature_desc=Inner`
|
|
22
|
+
|
|
23
|
+
function makeModel() {
|
|
24
|
+
const model = MSAModelF().create({
|
|
25
|
+
type: 'MsaView',
|
|
26
|
+
msaFormat: 'fasta',
|
|
27
|
+
data: { msa },
|
|
28
|
+
})
|
|
29
|
+
model.setWidth(800)
|
|
30
|
+
model.applyGFFText(gff)
|
|
31
|
+
return model
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sweep(model: ReturnType<typeof makeModel>, from = 0) {
|
|
35
|
+
const bandAt = domainBandCursor(model.domainBandsByStart.get('seq1'))
|
|
36
|
+
const out: (string | undefined)[] = []
|
|
37
|
+
for (let col = from; col < 20; col++) {
|
|
38
|
+
out.push(bandAt(col)?.annotation.accession)
|
|
39
|
+
}
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('the overlay paints largest-first, so stackIndex is paint order', () => {
|
|
44
|
+
const bands = makeModel().domainBands.get('seq1')!
|
|
45
|
+
expect(bands.map(b => [b.annotation.accession, b.stackIndex])).toEqual([
|
|
46
|
+
['PF_OUTER', 0],
|
|
47
|
+
['PF_INNER', 1],
|
|
48
|
+
])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('a nested band wins the columns it covers', () => {
|
|
52
|
+
const model = makeModel()
|
|
53
|
+
// PF_INNER is seq positions 8-12 (1-based), i.e. columns 7..11
|
|
54
|
+
expect(sweep(model)).toEqual([
|
|
55
|
+
...Array.from({ length: 7 }, () => 'PF_OUTER'),
|
|
56
|
+
...Array.from({ length: 5 }, () => 'PF_INNER'),
|
|
57
|
+
...Array.from({ length: 8 }, () => 'PF_OUTER'),
|
|
58
|
+
])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('a sweep starting mid-block picks up bands opened before it', () => {
|
|
62
|
+
// canvas blocks start at arbitrary columns, so the cursor has to catch up on
|
|
63
|
+
// its first call rather than assuming it began at column 0
|
|
64
|
+
const model = makeModel()
|
|
65
|
+
expect(sweep(model, 9)).toEqual([
|
|
66
|
+
'PF_INNER',
|
|
67
|
+
'PF_INNER',
|
|
68
|
+
'PF_INNER',
|
|
69
|
+
...Array.from({ length: 8 }, () => 'PF_OUTER'),
|
|
70
|
+
])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('columns outside every band have no covering band', () => {
|
|
74
|
+
const model = makeModel()
|
|
75
|
+
model.setFilter('PF_OUTER', false)
|
|
76
|
+
const bandAt = domainBandCursor(model.domainBandsByStart.get('seq1'))
|
|
77
|
+
expect(bandAt(0)).toBeUndefined()
|
|
78
|
+
expect(bandAt(9)?.annotation.accession).toBe('PF_INNER')
|
|
79
|
+
expect(bandAt(19)).toBeUndefined()
|
|
80
|
+
})
|
package/src/ggplotPalettes.ts
CHANGED
package/src/hierarchy.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { describe, expect, test } from 'vitest'
|
|
|
3
3
|
import {
|
|
4
4
|
calcDepthToLeaf,
|
|
5
5
|
collapse,
|
|
6
|
-
|
|
6
|
+
collapsedSubtreeMaxLength,
|
|
7
7
|
find,
|
|
8
8
|
findMaxBranchLen,
|
|
9
9
|
hierarchy,
|
|
@@ -124,7 +124,7 @@ describe('collapse', () => {
|
|
|
124
124
|
})
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
-
describe('
|
|
127
|
+
describe('collapsedSubtreeMaxLength', () => {
|
|
128
128
|
// root → A(1) → [ A1(2), C(3) → [ C1(1), C2(10) ] ]
|
|
129
129
|
function makeLenTree(): NodeWithIds {
|
|
130
130
|
return {
|
|
@@ -152,18 +152,18 @@ describe('collapsedSubtreeLengthExtent', () => {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
test('
|
|
155
|
+
test('cumulative branch length to the farthest tip below a node', () => {
|
|
156
156
|
const h = hierarchy(makeLenTree(), d => d.children)
|
|
157
157
|
const nodeA = find(h, n => n.data.id === 'A')!
|
|
158
158
|
collapse(nodeA)
|
|
159
|
-
// excludes A's own branch:
|
|
160
|
-
expect(
|
|
159
|
+
// excludes A's own branch: farthest tip C2 = 3 + 10 = 13, vs A1 = 2
|
|
160
|
+
expect(collapsedSubtreeMaxLength(nodeA)).toBe(13)
|
|
161
161
|
})
|
|
162
162
|
|
|
163
|
-
test('returns zero
|
|
163
|
+
test('returns zero for a node with no descendants', () => {
|
|
164
164
|
const h = hierarchy(makeLenTree(), d => d.children)
|
|
165
165
|
const leaf = find(h, n => n.data.id === 'A1')!
|
|
166
|
-
expect(
|
|
166
|
+
expect(collapsedSubtreeMaxLength(leaf)).toBe(0)
|
|
167
167
|
})
|
|
168
168
|
})
|
|
169
169
|
|
package/src/hierarchy.ts
CHANGED
|
@@ -28,9 +28,8 @@ export interface HierarchyNode<T = NodeWithIds> extends CoreHierarchyNode<T> {
|
|
|
28
28
|
len?: number
|
|
29
29
|
depthToLeaf?: number
|
|
30
30
|
_children?: HierarchyNode<T>[] | null
|
|
31
|
-
// pixel x-
|
|
32
|
-
//
|
|
33
|
-
collapsedTipXNear?: number
|
|
31
|
+
// pixel x-position of the farthest tip of a collapsed subtree, where the base
|
|
32
|
+
// of the collapsed-clade triangle goes. Set in the model's hierarchy getter.
|
|
34
33
|
collapsedTipXFar?: number
|
|
35
34
|
}
|
|
36
35
|
|
|
@@ -115,14 +114,13 @@ export function collapse<T>(node: HierarchyNode<T>) {
|
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
//
|
|
119
|
-
// its detached subtree (excludes the node's own branch).
|
|
117
|
+
// Cumulative raw branch length from a collapsed node down to the farthest tip of
|
|
118
|
+
// its detached subtree (excludes the node's own branch). Sizes the base of the
|
|
120
119
|
// collapsed-clade triangle in phylogram mode.
|
|
121
|
-
export function
|
|
120
|
+
export function collapsedSubtreeMaxLength<T extends { length?: number }>(
|
|
122
121
|
node: HierarchyNode<T>,
|
|
123
122
|
) {
|
|
124
123
|
const roots = node._children ?? node.children
|
|
125
|
-
let min = Infinity
|
|
126
124
|
let max = 0
|
|
127
125
|
if (roots) {
|
|
128
126
|
const stack = roots.map(child => ({
|
|
@@ -140,12 +138,11 @@ export function collapsedSubtreeLengthExtent<T extends { length?: number }>(
|
|
|
140
138
|
})
|
|
141
139
|
}
|
|
142
140
|
} else {
|
|
143
|
-
min = Math.min(min, acc)
|
|
144
141
|
max = Math.max(max, acc)
|
|
145
142
|
}
|
|
146
143
|
}
|
|
147
144
|
}
|
|
148
|
-
return
|
|
145
|
+
return max
|
|
149
146
|
}
|
|
150
147
|
|
|
151
148
|
// Cladogram positioning based on ape's plot.phylo: uses topological depth (max
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,9 @@ export { default as MSAViewer } from './components/MSAViewer.tsx'
|
|
|
11
11
|
export { type MsaViewModel, default as MSAModelF } from './model.ts'
|
|
12
12
|
export type { MSAFormat, MSAParserType } from 'msa-parsers'
|
|
13
13
|
export type { HierarchyNode } from './hierarchy.ts'
|
|
14
|
-
|
|
14
|
+
// InterProScanResults is the EBI wire format that setDomains still accepts;
|
|
15
|
+
// Annotation is the source-agnostic shape everything else is written against
|
|
16
|
+
export type { InterProScanResults } from 'msa-parsers'
|
|
15
17
|
// ColumnCounts, DomainBand and TidyDomainAnnotation surface in the inferred type
|
|
16
18
|
// of the composed state model, so a downstream plugin cannot emit declarations
|
|
17
19
|
// for its own stateModelFactory without being able to name them (TS2883).
|
|
@@ -19,6 +21,7 @@ export type { InterProScanResults } from './launchInterProScan.ts'
|
|
|
19
21
|
export type { ColumnCounts } from './columnCounts.ts'
|
|
20
22
|
export type {
|
|
21
23
|
Accession,
|
|
24
|
+
Annotation,
|
|
22
25
|
BasicTrack,
|
|
23
26
|
BasicTrackModel,
|
|
24
27
|
DomainBand,
|
package/src/largeTree.test.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { expect, test } from 'vitest'
|
|
|
3
3
|
|
|
4
4
|
import MSAModelF from './model.ts'
|
|
5
5
|
|
|
6
|
-
// jsdom has no 2d context,
|
|
7
|
-
// width matters here, so a stub keeps the test about tree size.
|
|
6
|
+
// jsdom has no 2d context, so measureTextCanvas falls back to an estimate. Only
|
|
7
|
+
// the width matters here, so a stub keeps the test about tree size.
|
|
8
8
|
HTMLCanvasElement.prototype.getContext = (() => ({
|
|
9
9
|
font: '12px sans-serif',
|
|
10
10
|
measureText: (t: string) => ({ width: t.length * 7 }),
|
|
@@ -30,3 +30,24 @@ test('a tree far past the argument limit still lays out', () => {
|
|
|
30
30
|
expect(model.labelsWidth).toBe(49)
|
|
31
31
|
expect(model.treeWidth).toBeGreaterThan(0)
|
|
32
32
|
})
|
|
33
|
+
|
|
34
|
+
test('re-hovering the same tree node does not re-walk the tree', () => {
|
|
35
|
+
const model = MSAModelF().create({
|
|
36
|
+
type: 'MsaView',
|
|
37
|
+
data: { tree: flatNewick(200_000) },
|
|
38
|
+
})
|
|
39
|
+
model.setWidth(1000)
|
|
40
|
+
|
|
41
|
+
const leafId = model.leaves[1000]!.data.id
|
|
42
|
+
model.setHoveredTreeNode(leafId)
|
|
43
|
+
const first = model.hoveredTreeNode
|
|
44
|
+
|
|
45
|
+
// the tree's mousemove handler fires this on every event, so an unchanged id
|
|
46
|
+
// has to leave the state object alone: rebuilding it walks all 200k nodes and
|
|
47
|
+
// invalidates every overlay that reads hoveredRowIndices
|
|
48
|
+
model.setHoveredTreeNode(leafId)
|
|
49
|
+
expect(model.hoveredTreeNode).toBe(first)
|
|
50
|
+
|
|
51
|
+
model.setHoveredTreeNode(undefined)
|
|
52
|
+
expect(model.hoveredTreeNode).toBeUndefined()
|
|
53
|
+
})
|
|
@@ -1,30 +1,13 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util'
|
|
2
|
+
import { interProScanResponseToAnnotations } from 'msa-parsers'
|
|
2
3
|
|
|
3
4
|
import { isAbortError, jsonfetch, textfetch, timeout } from './fetchUtils.ts'
|
|
4
5
|
|
|
5
6
|
import type { MsaViewModel } from './model.ts'
|
|
7
|
+
import type { InterProScanResponse } from 'msa-parsers'
|
|
6
8
|
|
|
7
9
|
const base = 'https://www.ebi.ac.uk/Tools/services/rest'
|
|
8
10
|
|
|
9
|
-
export interface InterProScanResults {
|
|
10
|
-
matches: {
|
|
11
|
-
signature: {
|
|
12
|
-
entry?: {
|
|
13
|
-
name: string
|
|
14
|
-
description: string
|
|
15
|
-
accession: string
|
|
16
|
-
// original GFF feature type (exon, CDS, ...) when sourced from GFF
|
|
17
|
-
featureType?: string
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
locations: { start: number; end: number; strand?: number }[]
|
|
21
|
-
}[]
|
|
22
|
-
xref: { id: string }[]
|
|
23
|
-
}
|
|
24
|
-
export interface InterProScanResponse {
|
|
25
|
-
results: InterProScanResults[]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
11
|
async function runInterProScan({
|
|
29
12
|
seq,
|
|
30
13
|
onProgress,
|
|
@@ -169,17 +152,7 @@ async function loadInterProScanResultsWithStatus({
|
|
|
169
152
|
url: `https://www.ebi.ac.uk/Tools/services/rest/iprscan5/result/${jobId}/json`,
|
|
170
153
|
})
|
|
171
154
|
const ret = await loadInterProScanResults(jobId, signal)
|
|
172
|
-
|
|
173
|
-
// rather than crashing the whole load on one malformed result
|
|
174
|
-
model.setDomains(
|
|
175
|
-
Object.fromEntries(
|
|
176
|
-
ret.results
|
|
177
|
-
.map(r => [r.xref[0]?.id, r] as const)
|
|
178
|
-
.filter(
|
|
179
|
-
(e): e is [string, InterProScanResults] => e[0] !== undefined,
|
|
180
|
-
),
|
|
181
|
-
),
|
|
182
|
-
)
|
|
155
|
+
model.setAnnotations(interProScanResponseToAnnotations(ret))
|
|
183
156
|
getSession(model).notify(`Loaded interproscan ${jobId} results`, 'success')
|
|
184
157
|
} catch (e) {
|
|
185
158
|
if (isAbortError(e)) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// What a failed load LOOKS like, which is a different question from what the
|
|
4
|
+
// model recorded. modelFilehandleLoaders.test.ts already pins the model half --
|
|
5
|
+
// a rejected fetch sets `error` and clears `loadingMSA`, under the name "a
|
|
6
|
+
// failed fetch surfaces as an error" -- and it passed throughout the whole time
|
|
7
|
+
// a failed url showed the reader a spinner and nothing else.
|
|
8
|
+
//
|
|
9
|
+
// The error display was never missing. ImportForm renders `model.error`, and
|
|
10
|
+
// the reader could not get to it: a non-abort failure KEEPS its filehandle
|
|
11
|
+
// (only an abort clears it), so `hasPendingFilehandle` stayed true and
|
|
12
|
+
// Loading.tsx took its spinner branch ahead of the import form, permanently.
|
|
13
|
+
// Found on AlphaFold's `files/msa/` prefix answering 403 to every key in August
|
|
14
|
+
// 2026 -- the view opened, said "Downloading file" with a Cancel button, and
|
|
15
|
+
// was still saying it at 105s.
|
|
16
|
+
import React, { act } from 'react'
|
|
17
|
+
|
|
18
|
+
import { createRoot } from 'react-dom/client'
|
|
19
|
+
import { afterEach, beforeEach, expect, test } from 'vitest'
|
|
20
|
+
|
|
21
|
+
import MSAView from './components/Loading.tsx'
|
|
22
|
+
import MSAModelF from './model.ts'
|
|
23
|
+
|
|
24
|
+
import type { MsaViewModel } from './model.ts'
|
|
25
|
+
import type { Root } from 'react-dom/client'
|
|
26
|
+
|
|
27
|
+
Reflect.set(globalThis, 'IS_REACT_ACT_ENVIRONMENT', true)
|
|
28
|
+
|
|
29
|
+
let container: HTMLDivElement
|
|
30
|
+
let root: Root
|
|
31
|
+
let model: MsaViewModel
|
|
32
|
+
|
|
33
|
+
function uri(u: string) {
|
|
34
|
+
return { locationType: 'UriLocation' as const, uri: u }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function render() {
|
|
38
|
+
act(() => {
|
|
39
|
+
root.render(<MSAView model={model} />)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
container = document.createElement('div')
|
|
45
|
+
document.body.append(container)
|
|
46
|
+
root = createRoot(container)
|
|
47
|
+
model = MSAModelF().create({ type: 'MsaView' })
|
|
48
|
+
model.setWidth(800)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
act(() => {
|
|
53
|
+
root.unmount()
|
|
54
|
+
})
|
|
55
|
+
container.remove()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('a load that fails shows the error rather than a spinner forever', () => {
|
|
59
|
+
// the state the loader leaves behind on a 403: the filehandle it could not
|
|
60
|
+
// read is still set, nothing parsed, and the error recorded
|
|
61
|
+
model.setMSAFilehandle(uri('https://example.com/blocked.a3m'))
|
|
62
|
+
model.setLoadingMSA(false)
|
|
63
|
+
model.setError(new Error('HTTP 403 fetching blocked.a3m'))
|
|
64
|
+
render()
|
|
65
|
+
|
|
66
|
+
expect(container.textContent).toContain('403')
|
|
67
|
+
expect(container.textContent).not.toContain('Loading')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('a load still in flight is a spinner, not an error', () => {
|
|
71
|
+
model.setMSAFilehandle(uri('https://example.com/slow.a3m'))
|
|
72
|
+
render()
|
|
73
|
+
|
|
74
|
+
expect(model.loadingMSA).toBe(true)
|
|
75
|
+
expect(container.textContent).not.toContain('Return to import form')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// A guard rather than a pin: this one passes without the fix too, because with
|
|
79
|
+
// no filehandle left set the view reaches the ImportForm and that already shows
|
|
80
|
+
// the error. It is here so the new branch cannot quietly swallow the case.
|
|
81
|
+
//
|
|
82
|
+
// It also records the surprise underneath: `dataInitialized` is itself
|
|
83
|
+
// `(msa || tree) && !error`, so ANY error hides a loaded alignment, including a
|
|
84
|
+
// tree or gff that 404s beside a good one -- setError is shared by every
|
|
85
|
+
// filehandle loader. Whether a failed OPTIONAL file should blank a good
|
|
86
|
+
// alignment is a real question and a wider one, since `dataInitialized` is read
|
|
87
|
+
// in several places; narrowing it is a design change rather than this fix.
|
|
88
|
+
test('an error over loaded data says so instead of a bare import form', () => {
|
|
89
|
+
const loaded = MSAModelF().create({
|
|
90
|
+
type: 'MsaView',
|
|
91
|
+
msaFormat: 'fasta',
|
|
92
|
+
height: 200,
|
|
93
|
+
data: { msa: '>seq1\nACDE\n>seq2\nACDF' },
|
|
94
|
+
})
|
|
95
|
+
loaded.setWidth(800)
|
|
96
|
+
loaded.setError(new Error('domains.gff 404'))
|
|
97
|
+
act(() => {
|
|
98
|
+
root.render(<MSAView model={loaded} />)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
expect(container.textContent).toContain('domains.gff 404')
|
|
102
|
+
})
|
package/src/measureTextCanvas.ts
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import { setFontSize } from './setFontSize.ts'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Cached across calls: labelWidthMap measures every leaf label on every layout,
|
|
4
|
+
// and both createElement and getContext are far more expensive than measureText.
|
|
5
|
+
// `null` records that this environment has no 2d context, so the lookup is not
|
|
6
|
+
// retried on every label.
|
|
7
|
+
let ctxHandle: CanvasRenderingContext2D | null | undefined
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
if (
|
|
7
|
-
|
|
9
|
+
function getCtx() {
|
|
10
|
+
if (ctxHandle === undefined) {
|
|
11
|
+
ctxHandle =
|
|
12
|
+
typeof document === 'undefined'
|
|
13
|
+
? null
|
|
14
|
+
: document.createElement('canvas').getContext('2d')
|
|
8
15
|
}
|
|
16
|
+
return ctxHandle
|
|
17
|
+
}
|
|
9
18
|
|
|
10
|
-
|
|
19
|
+
// Rough average glyph width as a fraction of the font size for the sans-serif
|
|
20
|
+
// stack the tree labels use. Only reached where there is no canvas to measure
|
|
21
|
+
// with (jsdom, SSR); estimating the label gutter beats throwing out of the
|
|
22
|
+
// layout getters that read this on every render.
|
|
23
|
+
const fallbackGlyphWidthRatio = 0.6
|
|
24
|
+
|
|
25
|
+
export function measureTextCanvas(text: string, fontSize: number) {
|
|
26
|
+
const ctx = getCtx()
|
|
11
27
|
if (!ctx) {
|
|
12
|
-
|
|
28
|
+
return text.length * fontSize * fallbackGlyphWidthRatio
|
|
13
29
|
}
|
|
14
30
|
setFontSize(ctx, fontSize)
|
|
15
31
|
return ctx.measureText(text).width
|