react-msaview 5.10.0 → 6.1.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/columnCounts.d.ts +7 -0
- package/dist/columnCounts.js +18 -0
- package/dist/columnCounts.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/DragHandle.d.ts +25 -0
- package/dist/components/DragHandle.js +49 -0
- package/dist/components/DragHandle.js.map +1 -0
- package/dist/components/Loading.js +2 -2
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAView.js +2 -2
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/ResizeHandles.d.ts +4 -2
- package/dist/components/ResizeHandles.js +35 -54
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/SequenceTextArea.js +3 -10
- package/dist/components/SequenceTextArea.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/InterProScanDialog.js +52 -129
- package/dist/components/dialogs/InterProScanDialog.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 +15 -0
- 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 +40 -23
- 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 +9 -3
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +12 -3
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +1 -2
- 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/msaRaster.d.ts +63 -0
- package/dist/components/msa/msaRaster.js +252 -0
- package/dist/components/msa/msaRaster.js.map +1 -0
- package/dist/components/msa/renderMSABlock.d.ts +2 -1
- package/dist/components/msa/renderMSABlock.js +27 -41
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +1 -2
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/msa/tileColor.d.ts +6 -0
- package/dist/components/msa/tileColor.js +35 -0
- package/dist/components/msa/tileColor.js.map +1 -0
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/tracks/TrackBlocks.d.ts +14 -0
- package/dist/components/tracks/TrackBlocks.js +65 -0
- package/dist/components/tracks/TrackBlocks.js.map +1 -0
- package/dist/components/tracks/{renderTracksSvg.d.ts → drawTracks.d.ts} +22 -10
- package/dist/components/tracks/drawTracks.js +193 -0
- package/dist/components/tracks/drawTracks.js.map +1 -0
- package/dist/components/tree/TreeCanvas.js +8 -2
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +7 -4
- 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.js +5 -2
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +8 -0
- package/dist/constants.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/DataModel.d.ts +1 -4
- package/dist/model/DataModel.js +8 -10
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model.d.ts +86 -107
- package/dist/model.js +238 -162
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +34 -18
- package/dist/neighborJoining.js.map +1 -1
- package/dist/renderTestEnv.d.ts +43 -0
- package/dist/renderTestEnv.js +88 -0
- package/dist/renderTestEnv.js.map +1 -0
- package/dist/renderToSvg.js +3 -3
- 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/sequenceLogo.d.ts +28 -0
- package/dist/sequenceLogo.js +47 -0
- package/dist/sequenceLogo.js.map +1 -0
- package/dist/types.d.ts +6 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +5 -5
- package/src/clustalX.ts +22 -22
- package/src/colSpace.test.ts +27 -0
- package/src/columnCounts.test.ts +15 -0
- package/src/columnCounts.ts +19 -0
- package/src/components/CopyButton.tsx +44 -0
- package/src/components/DragHandle.tsx +81 -0
- package/src/components/Loading.tsx +10 -1
- package/src/components/MSAView.tsx +2 -3
- package/src/components/ResizeHandles.tsx +70 -87
- package/src/components/SequenceTextArea.tsx +3 -16
- package/src/components/VerticalScrollbar.tsx +20 -33
- package/src/components/dialogs/FeatureDialog.tsx +3 -4
- package/src/components/dialogs/InterProScanDialog.tsx +54 -131
- 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 +18 -0
- package/src/components/import/ImportForm.tsx +1 -1
- package/src/components/import/ImportFormExamples.tsx +82 -133
- package/src/components/minimap/Minimap.tsx +67 -32
- 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 +24 -11
- package/src/components/msa/MSACanvasBlock.tsx +13 -3
- package/src/components/msa/MSAMouseoverCanvas.tsx +1 -3
- package/src/components/msa/MSAPanel.tsx +2 -2
- package/src/components/msa/domainBandCursor.ts +41 -0
- package/src/components/msa/msaRaster.test.ts +145 -0
- package/src/components/msa/msaRaster.ts +350 -0
- package/src/components/msa/renderMSABlock.ts +34 -43
- package/src/components/msa/renderMSAMouseover.ts +1 -3
- package/src/components/msa/tileColor.ts +37 -0
- package/src/components/renderCtx.ts +2 -0
- package/src/components/tracks/TrackBlocks.tsx +111 -0
- package/src/components/tracks/drawTracks.ts +323 -0
- package/src/components/tree/TreeCanvas.tsx +16 -4
- package/src/components/tree/TreeCanvasBlock.tsx +18 -11
- package/src/components/tree/TreeRuler.tsx +7 -2
- package/src/components/tree/renderTreeCanvas.test.ts +15 -7
- package/src/components/tree/renderTreeCanvas.ts +5 -1
- package/src/constants.ts +10 -0
- 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/impgRender.test.tsx +2 -57
- package/src/index.ts +4 -1
- package/src/largeTree.test.ts +39 -4
- package/src/launchInterProScan.ts +3 -30
- package/src/loadErrorDisplay.test.tsx +102 -0
- package/src/measureTextCanvas.ts +22 -6
- package/src/model/DataModel.ts +11 -11
- package/src/model.ts +262 -172
- package/src/neighborJoining.ts +36 -18
- package/src/renderDomainsSvg.test.tsx +2 -57
- package/src/renderMinimapSvg.test.tsx +55 -0
- package/src/renderSequenceLogoSvg.test.tsx +176 -0
- package/src/renderTestEnv.ts +96 -0
- package/src/renderToSvg.tsx +3 -3
- package/src/rowCoordinateCalculations.ts +48 -44
- package/src/sequenceLogo.test.ts +88 -0
- package/src/sequenceLogo.ts +63 -0
- package/src/types.ts +17 -16
- package/src/version.ts +1 -1
- package/src/viewChrome.test.tsx +134 -0
- package/dist/components/ConservationTrack.d.ts +0 -8
- package/dist/components/ConservationTrack.js +0 -49
- package/dist/components/ConservationTrack.js.map +0 -1
- package/dist/components/TextTrack.d.ts +0 -8
- package/dist/components/TextTrack.js +0 -58
- package/dist/components/TextTrack.js.map +0 -1
- 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/dist/components/tracks/renderTracksSvg.js +0 -126
- package/dist/components/tracks/renderTracksSvg.js.map +0 -1
- package/src/components/ConservationTrack.tsx +0 -99
- package/src/components/TextTrack.tsx +0 -103
- package/src/components/dialogs/DomainDialog.tsx +0 -45
- package/src/components/header/SettingsMenu.tsx +0 -57
- package/src/components/tracks/renderTracksSvg.ts +0 -247
|
@@ -3,6 +3,7 @@ import React, { useState } from 'react'
|
|
|
3
3
|
import { makeStyles } from '@jbrowse/core/util/tss-react'
|
|
4
4
|
import { useTheme } from '@mui/material'
|
|
5
5
|
import { observer } from 'mobx-react'
|
|
6
|
+
import { createPortal } from 'react-dom'
|
|
6
7
|
|
|
7
8
|
import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
|
|
8
9
|
import TreeBranchMenu from './TreeBranchMenu.tsx'
|
|
@@ -56,7 +57,7 @@ const TreeCanvasBlock = observer(function ({
|
|
|
56
57
|
const { clickMap, hovered, hitTest, onMouseMove, onMouseLeave } =
|
|
57
58
|
useTreeHover({ model, offsetY })
|
|
58
59
|
|
|
59
|
-
const {
|
|
60
|
+
const { treeAreaWidth, blockSize, highResScaleFactor } = model
|
|
60
61
|
const width = treeAreaWidth + padding
|
|
61
62
|
const height = blockSize
|
|
62
63
|
|
|
@@ -82,7 +83,7 @@ const TreeCanvasBlock = observer(function ({
|
|
|
82
83
|
const style = {
|
|
83
84
|
width,
|
|
84
85
|
height,
|
|
85
|
-
top:
|
|
86
|
+
top: offsetY,
|
|
86
87
|
left: 0,
|
|
87
88
|
position: 'absolute',
|
|
88
89
|
} as const
|
|
@@ -144,21 +145,27 @@ const TreeCanvasBlock = observer(function ({
|
|
|
144
145
|
className={classes.hover}
|
|
145
146
|
style={{
|
|
146
147
|
left: hovered.minX,
|
|
147
|
-
top: hovered.minY
|
|
148
|
+
top: hovered.minY,
|
|
148
149
|
width: hovered.maxX - hovered.minX,
|
|
149
150
|
height: hovered.maxY - hovered.minY,
|
|
150
151
|
}}
|
|
151
152
|
/>
|
|
152
153
|
) : null}
|
|
153
154
|
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
{/* portaled: the block set sits under a transform, which would otherwise
|
|
156
|
+
make this fixed tooltip position against the scrolled tree instead of the
|
|
157
|
+
window */}
|
|
158
|
+
{hovered
|
|
159
|
+
? createPortal(
|
|
160
|
+
<div
|
|
161
|
+
className={classes.tooltip}
|
|
162
|
+
style={{ left: hovered.clientX + 12, top: hovered.clientY + 12 }}
|
|
163
|
+
>
|
|
164
|
+
{hovered.name}
|
|
165
|
+
</div>,
|
|
166
|
+
document.body,
|
|
167
|
+
)
|
|
168
|
+
: null}
|
|
162
169
|
</>
|
|
163
170
|
)
|
|
164
171
|
})
|
|
@@ -4,9 +4,14 @@ import { observer } from 'mobx-react'
|
|
|
4
4
|
|
|
5
5
|
import type { MsaViewModel } from '../../model.ts'
|
|
6
6
|
|
|
7
|
+
// The gutter above the tree panel. It spans the resize handle too, so whatever
|
|
8
|
+
// follows it in the top row starts exactly where the alignment canvas does --
|
|
9
|
+
// MainArea has TreePanel + VerticalResizeHandle before the alignment.
|
|
7
10
|
const TreeRuler = observer(({ model }: { model: MsaViewModel }) => {
|
|
8
|
-
const { treeAreaWidth } = model
|
|
9
|
-
return
|
|
11
|
+
const { treeAreaWidth, resizeHandleWidth } = model
|
|
12
|
+
return (
|
|
13
|
+
<div style={{ flexShrink: 0, width: treeAreaWidth + resizeHandleWidth }} />
|
|
14
|
+
)
|
|
10
15
|
})
|
|
11
16
|
|
|
12
17
|
export default TreeRuler
|
|
@@ -117,8 +117,9 @@ describe('getNodeX cladogram positioning', () => {
|
|
|
117
117
|
})
|
|
118
118
|
})
|
|
119
119
|
|
|
120
|
-
// the x values the tree pass actually strokes, for a model built from newick
|
|
121
|
-
|
|
120
|
+
// the x values the tree pass actually strokes, for a model built from newick,
|
|
121
|
+
// alongside the tree width they are expected to span
|
|
122
|
+
function drawnTree(newick: string) {
|
|
122
123
|
const model = stateModelFactory().create({
|
|
123
124
|
type: 'MsaView',
|
|
124
125
|
data: { msa: '>a\nA\n>b\nA\n>c\nA\n>d\nA', tree: newick },
|
|
@@ -150,19 +151,26 @@ function drawnTreeXs(newick: string) {
|
|
|
150
151
|
offsetY: 0,
|
|
151
152
|
theme: { palette: { text: { primary: '#000' } } } as Theme,
|
|
152
153
|
})
|
|
153
|
-
return xs
|
|
154
|
+
return { xs, model }
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
describe('renderTreeCanvas horizontal extent', () => {
|
|
157
158
|
it('spreads a cladogram across the tree area when branch lengths are absent', () => {
|
|
158
159
|
// a lengthless newick forces cladogram mode; scaling it by the (zero) max
|
|
159
160
|
// branch length would stack every node on x=0 as one vertical line
|
|
160
|
-
const xs =
|
|
161
|
-
expect(Math.max(...xs)).toBe(
|
|
161
|
+
const { xs, model } = drawnTree('((a,b),(c,d));')
|
|
162
|
+
expect(Math.max(...xs)).toBe(model.treeWidth)
|
|
162
163
|
})
|
|
163
164
|
|
|
164
165
|
it('scales a phylogram by branch length', () => {
|
|
165
|
-
const xs =
|
|
166
|
-
expect(Math.max(...xs)).toBeCloseTo(
|
|
166
|
+
const { xs, model } = drawnTree('((a:0.1,b:0.2):0.3,(c:0.4,d:0.5):0.6);')
|
|
167
|
+
expect(Math.max(...xs)).toBeCloseTo(model.treeWidth)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// labels off means no label gutter to reserve, so the tips reach the far edge
|
|
171
|
+
// of the tree area rather than stopping at the default treeWidth
|
|
172
|
+
it('hands the label gutter to the tree when labels are off', () => {
|
|
173
|
+
const { model } = drawnTree('((a,b),(c,d));')
|
|
174
|
+
expect(model.treeWidth).toBe(model.treeAreaWidth - 10 - model.marginLeft)
|
|
167
175
|
})
|
|
168
176
|
})
|
|
@@ -264,6 +264,7 @@ function renderTreeLabels({
|
|
|
264
264
|
marginLeft,
|
|
265
265
|
noTree,
|
|
266
266
|
labelWidthMap,
|
|
267
|
+
labelWidthScale,
|
|
267
268
|
} = model
|
|
268
269
|
const by = blockSizeYOverride ?? blockSize
|
|
269
270
|
// labels only exist for leaves, which are laid out top to bottom, so take the
|
|
@@ -295,8 +296,11 @@ function renderTreeLabels({
|
|
|
295
296
|
xp = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf) ?? 0
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
const measured = labelWidthMap.get(name)
|
|
298
300
|
const width =
|
|
299
|
-
|
|
301
|
+
measured === undefined
|
|
302
|
+
? ctx.measureText(displayName).width
|
|
303
|
+
: measured * labelWidthScale
|
|
300
304
|
|
|
301
305
|
ctx.fillStyle = theme.palette.text.primary
|
|
302
306
|
if (labelsAlignRight) {
|
package/src/constants.ts
CHANGED
|
@@ -31,6 +31,16 @@ export const segmentShades = ['#9fb6d4', '#d4dcea']
|
|
|
31
31
|
export const defaultDrawMsaLetters = true
|
|
32
32
|
export const defaultScrollZoom = false
|
|
33
33
|
|
|
34
|
+
// Cell size floors for drawing residue letters and tree labels. Below these a
|
|
35
|
+
// 500px block holds thousands of glyphs, and fillText -- which no sprite atlas
|
|
36
|
+
// beats, measured -- dominates every zoom frame.
|
|
37
|
+
export const minLetterRowHeight = 8
|
|
38
|
+
export const minLetterColWidth = 5
|
|
39
|
+
|
|
40
|
+
// Tree labels are measured once at this size and scaled to the current font
|
|
41
|
+
// size, so a vertical zoom never re-measures the tree.
|
|
42
|
+
export const labelReferenceFontSize = 16
|
|
43
|
+
|
|
34
44
|
// Zoom limits, px per cell. maxCellSize caps both smooth zoom and the stepwise
|
|
35
45
|
// zoom-in buttons (which otherwise grow unbounded). minColWidth/minRowHeight are
|
|
36
46
|
// the smooth-zoom floors.
|
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/impgRender.test.tsx
CHANGED
|
@@ -12,67 +12,12 @@ import { enableStaticRendering } from 'mobx-react'
|
|
|
12
12
|
import { beforeAll, expect, test } from 'vitest'
|
|
13
13
|
|
|
14
14
|
import MSAModelF from './model.ts'
|
|
15
|
+
import { installRenderTestEnv } from './renderTestEnv.ts'
|
|
15
16
|
import { renderToSvg } from './renderToSvg.tsx'
|
|
16
17
|
|
|
17
|
-
class Mat {
|
|
18
|
-
constructor(
|
|
19
|
-
public a = 1,
|
|
20
|
-
public b = 0,
|
|
21
|
-
public c = 0,
|
|
22
|
-
public d = 1,
|
|
23
|
-
public e = 0,
|
|
24
|
-
public f = 0,
|
|
25
|
-
) {}
|
|
26
|
-
multiply(o: Mat) {
|
|
27
|
-
return new Mat(
|
|
28
|
-
this.a * o.a + this.c * o.b,
|
|
29
|
-
this.b * o.a + this.d * o.b,
|
|
30
|
-
this.a * o.c + this.c * o.d,
|
|
31
|
-
this.b * o.c + this.d * o.d,
|
|
32
|
-
this.a * o.e + this.c * o.f + this.e,
|
|
33
|
-
this.b * o.e + this.d * o.f + this.f,
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
translate(x: number, y = 0) {
|
|
37
|
-
return this.multiply(new Mat(1, 0, 0, 1, x, y))
|
|
38
|
-
}
|
|
39
|
-
scale(x: number, y = x) {
|
|
40
|
-
return this.multiply(new Mat(x, 0, 0, y, 0, 0))
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
class Pt {
|
|
44
|
-
constructor(
|
|
45
|
-
public x = 0,
|
|
46
|
-
public y = 0,
|
|
47
|
-
) {}
|
|
48
|
-
matrixTransform(m: Mat) {
|
|
49
|
-
return new Pt(
|
|
50
|
-
m.a * this.x + m.c * this.y + m.e,
|
|
51
|
-
m.b * this.x + m.d * this.y + m.f,
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
18
|
beforeAll(() => {
|
|
57
19
|
enableStaticRendering(true)
|
|
58
|
-
|
|
59
|
-
g.DOMMatrix = Mat
|
|
60
|
-
g.DOMPoint = Pt
|
|
61
|
-
HTMLCanvasElement.prototype.getContext = function () {
|
|
62
|
-
let font = '10px sans-serif'
|
|
63
|
-
return {
|
|
64
|
-
get font() {
|
|
65
|
-
return font
|
|
66
|
-
},
|
|
67
|
-
set font(v: string) {
|
|
68
|
-
font = v
|
|
69
|
-
},
|
|
70
|
-
measureText(t: string) {
|
|
71
|
-
const size = Number.parseFloat(font) || 10
|
|
72
|
-
return { width: t.length * size * 0.6 } as TextMetrics
|
|
73
|
-
},
|
|
74
|
-
} as unknown as CanvasRenderingContext2D
|
|
75
|
-
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
20
|
+
installRenderTestEnv()
|
|
76
21
|
})
|
|
77
22
|
|
|
78
23
|
// representative `impg query -o fasta-aln` block: PanSN names with region
|
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 }),
|
|
@@ -26,7 +26,42 @@ test('a tree far past the argument limit still lays out', () => {
|
|
|
26
26
|
model.setWidth(1000)
|
|
27
27
|
|
|
28
28
|
expect(model.numRows).toBe(200_000)
|
|
29
|
-
// 's199999' is the longest name, at 7 chars * 7px
|
|
30
|
-
|
|
29
|
+
// 's199999' is the longest name, at 7 chars * 7px, scaled from the reference
|
|
30
|
+
// font size the labels are measured at down to the current one (13/16)
|
|
31
|
+
expect(model.labelsWidth).toBeCloseTo((49 * 13) / 16)
|
|
31
32
|
expect(model.treeWidth).toBeGreaterThan(0)
|
|
32
33
|
})
|
|
34
|
+
|
|
35
|
+
test('re-hovering the same tree node does not re-walk the tree', () => {
|
|
36
|
+
const model = MSAModelF().create({
|
|
37
|
+
type: 'MsaView',
|
|
38
|
+
data: { tree: flatNewick(200_000) },
|
|
39
|
+
})
|
|
40
|
+
model.setWidth(1000)
|
|
41
|
+
|
|
42
|
+
const leafId = model.leaves[1000]!.data.id
|
|
43
|
+
model.setHoveredTreeNode(leafId)
|
|
44
|
+
const first = model.hoveredTreeNode
|
|
45
|
+
|
|
46
|
+
// the tree's mousemove handler fires this on every event, so an unchanged id
|
|
47
|
+
// has to leave the state object alone: rebuilding it walks all 200k nodes and
|
|
48
|
+
// invalidates every overlay that reads hoveredRowIndices
|
|
49
|
+
model.setHoveredTreeNode(leafId)
|
|
50
|
+
expect(model.hoveredTreeNode).toBe(first)
|
|
51
|
+
|
|
52
|
+
model.setHoveredTreeNode(undefined)
|
|
53
|
+
expect(model.hoveredTreeNode).toBeUndefined()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('a vertical zoom does not re-measure every label', () => {
|
|
57
|
+
const model = MSAModelF().create({
|
|
58
|
+
type: 'MsaView',
|
|
59
|
+
data: { tree: flatNewick(200_000) },
|
|
60
|
+
})
|
|
61
|
+
model.setWidth(1000)
|
|
62
|
+
|
|
63
|
+
const before = model.labelWidthMap
|
|
64
|
+
model.setRowHeight(24)
|
|
65
|
+
expect(model.labelWidthMap).toBe(before)
|
|
66
|
+
expect(model.labelsWidth).toBeCloseTo((49 * 18) / 16)
|
|
67
|
+
})
|
|
@@ -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)) {
|