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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// The reactive filehandle loaders in afterCreate: each fetches when its
|
|
2
|
+
// filehandle changes, and each guards against a stale response, since a user can
|
|
3
|
+
// pick a second file while the first is still downloading.
|
|
4
|
+
import { beforeEach, expect, test, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import MSAModelF from './model.ts'
|
|
7
|
+
|
|
8
|
+
import type * as FetchUtils from './fetchUtils.ts'
|
|
9
|
+
|
|
10
|
+
// uri -> settle functions for the fetch that autorun kicked off
|
|
11
|
+
const inFlight = new Map<
|
|
12
|
+
string,
|
|
13
|
+
{ resolve: (text: string) => void; reject: (e: unknown) => void }
|
|
14
|
+
>()
|
|
15
|
+
|
|
16
|
+
vi.mock('@jbrowse/core/util/io', () => ({
|
|
17
|
+
openLocation: (loc: { uri: string }) => loc,
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
vi.mock('./fetchUtils.ts', async importOriginal => ({
|
|
21
|
+
...(await importOriginal<typeof FetchUtils>()),
|
|
22
|
+
fetchTextWithProgress: (loc: { uri: string }) =>
|
|
23
|
+
new Promise<string>((resolve, reject) => {
|
|
24
|
+
inFlight.set(loc.uri, { resolve, reject })
|
|
25
|
+
}),
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
function uri(u: string) {
|
|
29
|
+
return { locationType: 'UriLocation' as const, uri: u }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fasta(seq: string) {
|
|
33
|
+
return `>seq1\n${seq}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// let the awaited fetch and the actions that follow it run
|
|
37
|
+
function flush() {
|
|
38
|
+
return new Promise(resolve => setTimeout(resolve, 0))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeModel() {
|
|
42
|
+
const model = MSAModelF().create({ type: 'MsaView' })
|
|
43
|
+
model.setWidth(800)
|
|
44
|
+
return model
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
inFlight.clear()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('an msa filehandle loads into the model and clears its loading flag', async () => {
|
|
52
|
+
const model = makeModel()
|
|
53
|
+
model.setMSAFilehandle(uri('aln.fa'))
|
|
54
|
+
expect(model.loadingMSA).toBe(true)
|
|
55
|
+
|
|
56
|
+
inFlight.get('aln.fa')!.resolve(fasta('ACDE'))
|
|
57
|
+
await flush()
|
|
58
|
+
|
|
59
|
+
expect(model.data.msa).toBe(fasta('ACDE'))
|
|
60
|
+
expect(model.loadingMSA).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a slower earlier fetch never clobbers a newer one', async () => {
|
|
64
|
+
const model = makeModel()
|
|
65
|
+
model.setMSAFilehandle(uri('first.fa'))
|
|
66
|
+
model.setMSAFilehandle(uri('second.fa'))
|
|
67
|
+
|
|
68
|
+
// the newer fetch finishes first, then the abandoned one comes back
|
|
69
|
+
inFlight.get('second.fa')!.resolve(fasta('SECOND'))
|
|
70
|
+
await flush()
|
|
71
|
+
inFlight.get('first.fa')!.resolve(fasta('FIRST'))
|
|
72
|
+
await flush()
|
|
73
|
+
|
|
74
|
+
expect(model.data.msa).toBe(fasta('SECOND'))
|
|
75
|
+
expect(model.loadingMSA).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('a stale failure does not raise an error over live data', async () => {
|
|
79
|
+
const model = makeModel()
|
|
80
|
+
model.setMSAFilehandle(uri('first.fa'))
|
|
81
|
+
model.setMSAFilehandle(uri('second.fa'))
|
|
82
|
+
|
|
83
|
+
inFlight.get('second.fa')!.resolve(fasta('SECOND'))
|
|
84
|
+
await flush()
|
|
85
|
+
inFlight.get('first.fa')!.reject(new Error('too slow'))
|
|
86
|
+
await flush()
|
|
87
|
+
|
|
88
|
+
expect(model.error).toBeUndefined()
|
|
89
|
+
expect(model.data.msa).toBe(fasta('SECOND'))
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('cancelling drops the filehandle so the view returns to the import form', async () => {
|
|
93
|
+
const model = makeModel()
|
|
94
|
+
model.setMSAFilehandle(uri('aln.fa'))
|
|
95
|
+
|
|
96
|
+
inFlight.get('aln.fa')!.reject(new DOMException('Aborted', 'AbortError'))
|
|
97
|
+
await flush()
|
|
98
|
+
|
|
99
|
+
expect(model.msaFilehandle).toBeUndefined()
|
|
100
|
+
expect(model.error).toBeUndefined()
|
|
101
|
+
expect(model.loadingMSA).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('a failed fetch surfaces as an error', async () => {
|
|
105
|
+
const model = makeModel()
|
|
106
|
+
model.setMSAFilehandle(uri('aln.fa'))
|
|
107
|
+
|
|
108
|
+
inFlight.get('aln.fa')!.reject(new Error('404'))
|
|
109
|
+
await flush()
|
|
110
|
+
|
|
111
|
+
expect(model.error).toBeInstanceOf(Error)
|
|
112
|
+
expect(model.loadingMSA).toBe(false)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('tree, treeMetadata and gff filehandles each load into their own field', async () => {
|
|
116
|
+
const model = MSAModelF().create({
|
|
117
|
+
type: 'MsaView',
|
|
118
|
+
treeFilehandle: uri('tree.nh'),
|
|
119
|
+
treeMetadataFilehandle: uri('meta.json'),
|
|
120
|
+
gffFilehandle: uri('domains.gff'),
|
|
121
|
+
})
|
|
122
|
+
model.setWidth(800)
|
|
123
|
+
|
|
124
|
+
inFlight.get('tree.nh')!.resolve('(a,b);')
|
|
125
|
+
inFlight.get('meta.json')!.resolve('{"a":{"genome":"human"}}')
|
|
126
|
+
inFlight
|
|
127
|
+
.get('domains.gff')!
|
|
128
|
+
.resolve(
|
|
129
|
+
'##gff-version 3\na\tPfam\tprotein_match\t1\t2\t.\t.\t.\tName=PF00069',
|
|
130
|
+
)
|
|
131
|
+
await flush()
|
|
132
|
+
|
|
133
|
+
expect(model.data.tree).toBe('(a,b);')
|
|
134
|
+
expect(model.treeMetadata.a?.genome).toBe('human')
|
|
135
|
+
expect(model.actuallyShowDomains).toBe(true)
|
|
136
|
+
expect(model.loadingTree).toBe(false)
|
|
137
|
+
})
|
|
@@ -149,4 +149,16 @@ describe('calculateNeighborJoiningTree', () => {
|
|
|
149
149
|
expect(tree).toContain('similar2')
|
|
150
150
|
expect(tree).toContain('different')
|
|
151
151
|
})
|
|
152
|
+
|
|
153
|
+
test('handles ragged rows, treating a short row as gapped past its end', () => {
|
|
154
|
+
const rows: [string, string][] = [
|
|
155
|
+
['full', 'MKAAYLSMFGKED'],
|
|
156
|
+
['short', 'MKAAYL'],
|
|
157
|
+
['other', 'WWWWWWWWWWWWW'],
|
|
158
|
+
]
|
|
159
|
+
const tree = calculateNeighborJoiningTree(rows)
|
|
160
|
+
|
|
161
|
+
expect(tree).toMatch(/;$/)
|
|
162
|
+
expect(tree).toContain('short')
|
|
163
|
+
})
|
|
152
164
|
})
|
package/src/neighborJoining.ts
CHANGED
|
@@ -53,18 +53,18 @@ function isGap(c: string) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function computePairwiseDistance(seq1: string, seq2: string) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
let
|
|
56
|
+
// ragged input is real -- a3m and hand-edited fasta both produce rows shorter
|
|
57
|
+
// than the alignment -- so a row that stops early counts as gapped for the
|
|
58
|
+
// remainder, the same rule the model's `blanks` getter applies. Requiring
|
|
59
|
+
// equal lengths here instead made "build tree from MSA" throw on those files.
|
|
60
|
+
const len = Math.max(seq1.length, seq2.length)
|
|
61
|
+
let compared = 0
|
|
62
62
|
let totalScore = 0
|
|
63
63
|
let maxPossibleScore = 0
|
|
64
64
|
|
|
65
|
-
for (let i = 0; i <
|
|
66
|
-
const a = seq1[i]
|
|
67
|
-
const b = seq2[i]
|
|
65
|
+
for (let i = 0; i < len; i++) {
|
|
66
|
+
const a = seq1[i] ?? '-'
|
|
67
|
+
const b = seq2[i] ?? '-'
|
|
68
68
|
|
|
69
69
|
const aGap = isGap(a)
|
|
70
70
|
const bGap = isGap(b)
|
|
@@ -72,24 +72,16 @@ function computePairwiseDistance(seq1: string, seq2: string) {
|
|
|
72
72
|
continue
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
compared++
|
|
75
76
|
if (aGap || bGap) {
|
|
76
|
-
mismatches++
|
|
77
77
|
continue
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
totalScore += score
|
|
80
|
+
totalScore += getBlosum62Score(a, b)
|
|
82
81
|
maxPossibleScore += Math.max(getBlosum62Score(a, a), getBlosum62Score(b, b))
|
|
83
|
-
|
|
84
|
-
if (a.toUpperCase() === b.toUpperCase()) {
|
|
85
|
-
matches++
|
|
86
|
-
} else {
|
|
87
|
-
mismatches++
|
|
88
|
-
}
|
|
89
82
|
}
|
|
90
83
|
|
|
91
|
-
|
|
92
|
-
if (total === 0) {
|
|
84
|
+
if (compared === 0) {
|
|
93
85
|
return 1
|
|
94
86
|
}
|
|
95
87
|
|
package/src/parseAsn1.ts
CHANGED
|
@@ -32,28 +32,38 @@ const remap: Record<string, string> = {
|
|
|
32
32
|
'leaf-count': 'leafCount',
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// index just past the '}' closing the '{' at openIdx, or -1 when it never
|
|
36
|
+
// closes. Braces nest (a node holds features, which hold values), so finding the
|
|
37
|
+
// end of a block means counting depth rather than taking the next '}'.
|
|
38
|
+
function afterMatchingBrace(str: string, openIdx: number) {
|
|
39
|
+
let depth = 1
|
|
40
|
+
let pos = openIdx + 1
|
|
41
|
+
while (pos < str.length && depth > 0) {
|
|
42
|
+
if (str[pos] === '{') {
|
|
43
|
+
depth++
|
|
44
|
+
} else if (str[pos] === '}') {
|
|
45
|
+
depth--
|
|
46
|
+
}
|
|
47
|
+
pos++
|
|
48
|
+
}
|
|
49
|
+
return depth === 0 ? pos : -1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// the contents of each top-level {...} block in str, siblings only
|
|
35
53
|
function extractBracedBlocks(str: string): string[] {
|
|
36
54
|
const blocks: string[] = []
|
|
37
55
|
let pos = 0
|
|
38
56
|
while (pos < str.length) {
|
|
39
|
-
|
|
40
|
-
if (
|
|
57
|
+
const openIdx = str.indexOf('{', pos)
|
|
58
|
+
if (openIdx === -1) {
|
|
41
59
|
break
|
|
42
60
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
while (pos < str.length && depth > 0) {
|
|
47
|
-
if (str[pos] === '{') {
|
|
48
|
-
depth++
|
|
49
|
-
} else if (str[pos] === '}') {
|
|
50
|
-
depth--
|
|
51
|
-
}
|
|
52
|
-
pos++
|
|
53
|
-
}
|
|
54
|
-
if (depth === 0) {
|
|
55
|
-
blocks.push(str.slice(start, pos - 1).trim())
|
|
61
|
+
const end = afterMatchingBrace(str, openIdx)
|
|
62
|
+
if (end === -1) {
|
|
63
|
+
break
|
|
56
64
|
}
|
|
65
|
+
blocks.push(str.slice(openIdx + 1, end - 1).trim())
|
|
66
|
+
pos = end
|
|
57
67
|
}
|
|
58
68
|
return blocks
|
|
59
69
|
}
|
|
@@ -63,17 +73,8 @@ function findBracedContent(str: string, after: number) {
|
|
|
63
73
|
if (openIdx === -1) {
|
|
64
74
|
return undefined
|
|
65
75
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
while (pos < str.length && depth > 0) {
|
|
69
|
-
if (str[pos] === '{') {
|
|
70
|
-
depth++
|
|
71
|
-
} else if (str[pos] === '}') {
|
|
72
|
-
depth--
|
|
73
|
-
}
|
|
74
|
-
pos++
|
|
75
|
-
}
|
|
76
|
-
return depth === 0 ? str.slice(openIdx + 1, pos - 1).trim() : undefined
|
|
76
|
+
const end = afterMatchingBrace(str, openIdx)
|
|
77
|
+
return end === -1 ? undefined : str.slice(openIdx + 1, end - 1).trim()
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
export function parseAsn1(
|
|
@@ -163,22 +164,13 @@ function extractSections(asnString: string): Record<string, string> {
|
|
|
163
164
|
continue
|
|
164
165
|
}
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
while (pos < content.length && depth > 0) {
|
|
170
|
-
if (content[pos] === '{') {
|
|
171
|
-
depth++
|
|
172
|
-
} else if (content[pos] === '}') {
|
|
173
|
-
depth--
|
|
174
|
-
}
|
|
175
|
-
pos++
|
|
176
|
-
}
|
|
177
|
-
if (depth === 0) {
|
|
178
|
-
sections[name] = content.slice(start, pos - 1).trim()
|
|
167
|
+
const end = afterMatchingBrace(content, pos)
|
|
168
|
+
if (end === -1) {
|
|
169
|
+
break
|
|
179
170
|
}
|
|
171
|
+
sections[name] = content.slice(pos + 1, end - 1).trim()
|
|
180
172
|
|
|
181
|
-
pos = content.indexOf(',',
|
|
173
|
+
pos = content.indexOf(',', end)
|
|
182
174
|
pos = pos === -1 ? content.length : pos + 1
|
|
183
175
|
}
|
|
184
176
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
//
|
|
3
|
-
// Verifies the
|
|
4
|
-
//
|
|
5
|
-
// impgRender.test.tsx (svgcanvas needs
|
|
3
|
+
// Verifies the SVG export draws the domain overlay: the boxes themselves across
|
|
4
|
+
// the full alignment width, and the color-key legend as a reserved column on the
|
|
5
|
+
// right. Polyfills mirror impgRender.test.tsx (svgcanvas needs
|
|
6
|
+
// DOMMatrix/DOMPoint + a measureText).
|
|
6
7
|
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
7
8
|
import { enableStaticRendering } from 'mobx-react'
|
|
8
9
|
import { beforeAll, expect, test } from 'vitest'
|
|
@@ -76,13 +77,17 @@ ACDEFGHIKL
|
|
|
76
77
|
>seq2
|
|
77
78
|
ACDE-GHIKL`
|
|
78
79
|
|
|
79
|
-
function makeModel(
|
|
80
|
+
function makeModel({
|
|
81
|
+
data = msa,
|
|
82
|
+
start = 1,
|
|
83
|
+
end = 5,
|
|
84
|
+
}: { data?: string; start?: number; end?: number } = {}) {
|
|
80
85
|
const model = MSAModelF().create({
|
|
81
|
-
id: 'domain-
|
|
86
|
+
id: 'domain-svg-test',
|
|
82
87
|
type: 'MsaView',
|
|
83
88
|
height: 400,
|
|
84
89
|
msaFormat: 'fasta',
|
|
85
|
-
data: { msa },
|
|
90
|
+
data: { msa: data },
|
|
86
91
|
})
|
|
87
92
|
model.setWidth(800)
|
|
88
93
|
model.setDomains({
|
|
@@ -97,7 +102,7 @@ function makeModel() {
|
|
|
97
102
|
description: 'Protein kinase domain',
|
|
98
103
|
},
|
|
99
104
|
},
|
|
100
|
-
locations: [{ start
|
|
105
|
+
locations: [{ start, end }],
|
|
101
106
|
},
|
|
102
107
|
],
|
|
103
108
|
},
|
|
@@ -105,18 +110,21 @@ function makeModel() {
|
|
|
105
110
|
return model
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
expect(model.actuallyShowDomains).toBe(true)
|
|
111
|
-
expect(model.visibleDomainTypes.map(d => d.accession)).toEqual(['PF00069'])
|
|
112
|
-
|
|
113
|
-
const svg = await renderToSvg(model, {
|
|
113
|
+
function exportEntire(model: ReturnType<typeof makeModel>) {
|
|
114
|
+
return renderToSvg(model, {
|
|
114
115
|
theme: createJBrowseTheme(),
|
|
115
116
|
exportType: 'entire',
|
|
116
117
|
includeMinimap: false,
|
|
117
118
|
includeTracks: false,
|
|
118
119
|
})
|
|
119
|
-
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
test('domain legend is drawn in the svg export when domains are shown', async () => {
|
|
123
|
+
const model = makeModel()
|
|
124
|
+
expect(model.actuallyShowDomains).toBe(true)
|
|
125
|
+
expect(model.visibleDomainTypes.map(d => d.accession)).toEqual(['PF00069'])
|
|
126
|
+
|
|
127
|
+
expect(await exportEntire(model)).toContain('Kinase')
|
|
120
128
|
})
|
|
121
129
|
|
|
122
130
|
test('no legend column when domains are hidden', async () => {
|
|
@@ -124,11 +132,27 @@ test('no legend column when domains are hidden', async () => {
|
|
|
124
132
|
model.setShowDomains(false)
|
|
125
133
|
expect(model.actuallyShowDomains).toBe(false)
|
|
126
134
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
expect(await exportEntire(model)).not.toContain('Kinase')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('a domain box past the on-screen block size still draws', async () => {
|
|
139
|
+
// the export renders the alignment in one pass, so the overlay has to be told
|
|
140
|
+
// how wide that pass is; falling back to the 500px on-screen block size culled
|
|
141
|
+
// every band to the left edge of a wide alignment
|
|
142
|
+
const row = 'ACDEFGHIKL'.repeat(20)
|
|
143
|
+
const model = makeModel({
|
|
144
|
+
data: `>seq1\n${row}\n>seq2\n${row}`,
|
|
145
|
+
start: 151,
|
|
146
|
+
end: 190,
|
|
132
147
|
})
|
|
133
|
-
|
|
148
|
+
const band = model.domainBands.get('seq1')![0]!
|
|
149
|
+
const x = band.startCol * model.colWidth
|
|
150
|
+
expect(x).toBeGreaterThan(model.blockSize)
|
|
151
|
+
|
|
152
|
+
const svg = await exportEntire(model)
|
|
153
|
+
const fill = model.fillPalette.PF00069!
|
|
154
|
+
const rects = [...svg.matchAll(/<rect[^>]*>/g)].map(m => m[0])
|
|
155
|
+
expect(
|
|
156
|
+
rects.some(r => r.includes(`fill="${fill}"`) && r.includes(`x="${x}"`)),
|
|
157
|
+
).toBe(true)
|
|
134
158
|
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// The viewport export draws a minimap by default, and nothing covered it. The
|
|
4
|
+
// export lays the figure out on its own grid rather than the on-screen one, so
|
|
5
|
+
// the minimap has to be told how wide to draw -- reading a width off the model
|
|
6
|
+
// puts its trapezoid short of the alignment column beside it.
|
|
7
|
+
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
8
|
+
import { enableStaticRendering } from 'mobx-react'
|
|
9
|
+
import { beforeAll, expect, test } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import MSAModelF from './model.ts'
|
|
12
|
+
import { renderToSvg } from './renderToSvg.tsx'
|
|
13
|
+
|
|
14
|
+
class Mat {
|
|
15
|
+
constructor(
|
|
16
|
+
public a = 1,
|
|
17
|
+
public b = 0,
|
|
18
|
+
public c = 0,
|
|
19
|
+
public d = 1,
|
|
20
|
+
public e = 0,
|
|
21
|
+
public f = 0,
|
|
22
|
+
) {}
|
|
23
|
+
multiply(o: Mat) {
|
|
24
|
+
return new Mat(
|
|
25
|
+
this.a * o.a + this.c * o.b,
|
|
26
|
+
this.b * o.a + this.d * o.b,
|
|
27
|
+
this.a * o.c + this.c * o.d,
|
|
28
|
+
this.b * o.c + this.d * o.d,
|
|
29
|
+
this.a * o.e + this.c * o.f + this.e,
|
|
30
|
+
this.b * o.e + this.d * o.f + this.f,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
translate(x: number, y = 0) {
|
|
34
|
+
return this.multiply(new Mat(1, 0, 0, 1, x, y))
|
|
35
|
+
}
|
|
36
|
+
scale(x: number, y = x) {
|
|
37
|
+
return this.multiply(new Mat(x, 0, 0, y, 0, 0))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
class Pt {
|
|
41
|
+
constructor(
|
|
42
|
+
public x = 0,
|
|
43
|
+
public y = 0,
|
|
44
|
+
) {}
|
|
45
|
+
matrixTransform(m: Mat) {
|
|
46
|
+
return new Pt(
|
|
47
|
+
m.a * this.x + m.c * this.y + m.e,
|
|
48
|
+
m.b * this.x + m.d * this.y + m.f,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
beforeAll(() => {
|
|
54
|
+
enableStaticRendering(true)
|
|
55
|
+
const g = globalThis as Record<string, unknown>
|
|
56
|
+
g.DOMMatrix = Mat
|
|
57
|
+
g.DOMPoint = Pt
|
|
58
|
+
HTMLCanvasElement.prototype.getContext = function () {
|
|
59
|
+
let font = '10px sans-serif'
|
|
60
|
+
return {
|
|
61
|
+
get font() {
|
|
62
|
+
return font
|
|
63
|
+
},
|
|
64
|
+
set font(v: string) {
|
|
65
|
+
font = v
|
|
66
|
+
},
|
|
67
|
+
measureText: (t: string) => ({
|
|
68
|
+
width: t.length * (Number.parseFloat(font) || 10) * 0.6,
|
|
69
|
+
}),
|
|
70
|
+
} as unknown as CanvasRenderingContext2D
|
|
71
|
+
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const width = 1000
|
|
75
|
+
|
|
76
|
+
function makeModel() {
|
|
77
|
+
const row = 'ACDEFGHIKL'.repeat(40)
|
|
78
|
+
const model = MSAModelF().create({
|
|
79
|
+
type: 'MsaView',
|
|
80
|
+
msaFormat: 'fasta',
|
|
81
|
+
height: 300,
|
|
82
|
+
data: { msa: `>seq1\n${row}\n>seq2\n${row}` },
|
|
83
|
+
})
|
|
84
|
+
model.setWidth(width)
|
|
85
|
+
return model
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
test('the exported minimap spans the exported alignment column', async () => {
|
|
89
|
+
const model = makeModel()
|
|
90
|
+
expect(model.showHorizontalScrollbar).toBe(true)
|
|
91
|
+
|
|
92
|
+
const svg = await renderToSvg(model, {
|
|
93
|
+
theme: createJBrowseTheme(),
|
|
94
|
+
exportType: 'viewport',
|
|
95
|
+
includeMinimap: true,
|
|
96
|
+
includeTracks: false,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// the minimap column runs from the tree area to the right edge of the figure
|
|
100
|
+
const minimapWidth = width - model.treeAreaWidth
|
|
101
|
+
const polygon = /<polygon[^>]*points="([^"]*)"/.exec(svg)?.[1]
|
|
102
|
+
expect(polygon).toBeDefined()
|
|
103
|
+
// bottom edge of the trapezoid reaches the full width it was given
|
|
104
|
+
expect(polygon!.endsWith(`${minimapWidth},${model.minimapHeight - 12}`)).toBe(
|
|
105
|
+
true,
|
|
106
|
+
)
|
|
107
|
+
// and the bar outline is drawn to the same width
|
|
108
|
+
expect(svg).toContain(`width="${minimapWidth}"`)
|
|
109
|
+
})
|
package/src/renderToSvg.tsx
CHANGED
|
@@ -161,7 +161,7 @@ function MsaSvg({
|
|
|
161
161
|
{includeMinimap ? (
|
|
162
162
|
<>
|
|
163
163
|
<g transform={`translate(${treeAreaWidth} 0)`}>
|
|
164
|
-
<MinimapSVG model={model} />
|
|
164
|
+
<MinimapSVG model={model} width={width - treeAreaWidth} />
|
|
165
165
|
</g>
|
|
166
166
|
<g transform={`translate(0 ${minimapHeight})`}>{body}</g>
|
|
167
167
|
</>
|
|
@@ -178,7 +178,7 @@ function MsaSvg({
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// the domain color key drawn as a reserved column to the right of the
|
|
181
|
-
// alignment, mirroring the on-screen
|
|
181
|
+
// alignment, mirroring the on-screen AnnotationLegend overlay
|
|
182
182
|
function LegendSVG({ model, width }: { model: MsaViewModel; width: number }) {
|
|
183
183
|
const { visibleDomainTypes, fillPalette, strokePalette } = model
|
|
184
184
|
const boxHeight = LEGEND_PAD * 2 + visibleDomainTypes.length * LEGEND_ROW_H
|
|
@@ -251,6 +251,9 @@ function CoreRendering({
|
|
|
251
251
|
ctx: msaCtx,
|
|
252
252
|
offsetX,
|
|
253
253
|
offsetY,
|
|
254
|
+
// the overlay culls bands to this width; without it the export falls back
|
|
255
|
+
// to the on-screen 500px block size and drops every band past it
|
|
256
|
+
blockSizeXOverride: msaAreaWidth,
|
|
254
257
|
blockSizeYOverride: contentHeight,
|
|
255
258
|
highResScaleFactorOverride: 1,
|
|
256
259
|
})
|
|
@@ -18,6 +18,25 @@ import { isBlank } from './util.ts'
|
|
|
18
18
|
* Each row can have different seqPos values for the same globalCol due to gaps.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Index of the first element in `[0, len)` for which `pred` holds, over a range
|
|
23
|
+
* where `pred` is false up to some point and true from there on. Both
|
|
24
|
+
* conversions below are that shape, since `blanks` is sorted ascending.
|
|
25
|
+
*/
|
|
26
|
+
function partitionPoint(len: number, pred: (i: number) => boolean) {
|
|
27
|
+
let left = 0
|
|
28
|
+
let right = len
|
|
29
|
+
while (left < right) {
|
|
30
|
+
const mid = (left + right) >>> 1
|
|
31
|
+
if (pred(mid)) {
|
|
32
|
+
right = mid
|
|
33
|
+
} else {
|
|
34
|
+
left = mid + 1
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return left
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
/**
|
|
22
41
|
* Convert a visible column index to a global column index.
|
|
23
42
|
* This is used when translating mouse/screen coordinates to MSA coordinates.
|
|
@@ -27,32 +46,12 @@ import { isBlank } from './util.ts'
|
|
|
27
46
|
* @returns The corresponding global column index in the full MSA
|
|
28
47
|
*/
|
|
29
48
|
export function visibleColToGlobalCol(blanks: number[], visibleCol: number) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
while (blankArrayIndex < blanksLen && blanks[blankArrayIndex] === globalCol) {
|
|
37
|
-
blankArrayIndex++
|
|
38
|
-
globalCol++
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
while (currentVisibleCol < visibleCol) {
|
|
42
|
-
currentVisibleCol++
|
|
43
|
-
globalCol++
|
|
44
|
-
|
|
45
|
-
// Skip any blank columns after incrementing
|
|
46
|
-
while (
|
|
47
|
-
blankArrayIndex < blanksLen &&
|
|
48
|
-
blanks[blankArrayIndex] === globalCol
|
|
49
|
-
) {
|
|
50
|
-
blankArrayIndex++
|
|
51
|
-
globalCol++
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return globalCol
|
|
49
|
+
// `blanks[i] - i` counts the visible columns before the i-th blank, so the
|
|
50
|
+
// first blank with more than visibleCol of them in front of it is the first
|
|
51
|
+
// one sitting past this column. Its index is how many blanks shift it right.
|
|
52
|
+
return (
|
|
53
|
+
visibleCol + partitionPoint(blanks.length, i => blanks[i]! - i > visibleCol)
|
|
54
|
+
)
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
/**
|
|
@@ -67,25 +66,30 @@ export function globalColToVisibleCol(
|
|
|
67
66
|
blanks: number[],
|
|
68
67
|
globalCol: number,
|
|
69
68
|
): number | undefined {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const mid = Math.floor((left + right) / 2)
|
|
76
|
-
if (blanks[mid] === globalCol) {
|
|
77
|
-
return undefined // Column is hidden
|
|
78
|
-
}
|
|
79
|
-
if (blanks[mid]! < globalCol) {
|
|
80
|
-
left = mid + 1
|
|
81
|
-
} else {
|
|
82
|
-
right = mid - 1
|
|
83
|
-
}
|
|
84
|
-
}
|
|
69
|
+
const blanksBefore = countBlanksBefore(blanks, globalCol)
|
|
70
|
+
return blanks[blanksBefore] === globalCol
|
|
71
|
+
? undefined // Column is hidden
|
|
72
|
+
: globalCol - blanksBefore
|
|
73
|
+
}
|
|
85
74
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
75
|
+
function countBlanksBefore(blanks: number[], globalCol: number) {
|
|
76
|
+
return partitionPoint(blanks.length, i => blanks[i]! >= globalCol)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* How many visible columns come before a global column. For a visible column
|
|
81
|
+
* that is its own visible index; for a hidden one it is the index of the next
|
|
82
|
+
* visible column, i.e. where the hidden column collapses to.
|
|
83
|
+
*
|
|
84
|
+
* That total answer is what spans want: resolving both ends of a span through
|
|
85
|
+
* globalColToVisibleCol instead loses the whole span whenever either end lands
|
|
86
|
+
* in a hidden column.
|
|
87
|
+
*
|
|
88
|
+
* @param blanks - Sorted array of global column indices that are hidden
|
|
89
|
+
* @param globalCol - The global column index in the full MSA
|
|
90
|
+
*/
|
|
91
|
+
export function visibleColsBefore(blanks: number[], globalCol: number) {
|
|
92
|
+
return globalCol - countBlanksBefore(blanks, globalCol)
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
/**
|