react-msaview 5.8.0 → 5.10.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 +84 -84
- package/bundle/index.js.map +3 -3
- package/dist/components/ConservationTrack.js +21 -16
- package/dist/components/ConservationTrack.js.map +1 -1
- package/dist/components/MSAView.js +17 -23
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/TextTrack.js +26 -21
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +39 -72
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/msa/DomainLegend.js +4 -5
- package/dist/components/msa/DomainLegend.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 +13 -6
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +9 -6
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +31 -31
- 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/renderTreeCanvas.d.ts +0 -38
- package/dist/components/tree/renderTreeCanvas.js +17 -17
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/model.d.ts +16 -1
- package/dist/model.js +99 -114
- 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 +3 -0
- package/dist/renderToSvg.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/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/dist/version.js.map +1 -1
- package/package.json +3 -3
- package/src/components/ConservationTrack.tsx +7 -5
- package/src/components/MSAView.tsx +24 -30
- package/src/components/TextTrack.tsx +7 -12
- package/src/components/header/settingsMenuItems.ts +40 -72
- package/src/components/msa/DomainLegend.tsx +4 -3
- package/src/components/msa/MSACanvasBlock.tsx +16 -21
- package/src/components/msa/MSAMouseoverCanvas.tsx +10 -6
- package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +51 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +13 -8
- package/src/components/msa/renderMSAMouseover.ts +37 -32
- 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/renderTreeCanvas.ts +20 -17
- package/src/constants.ts +1 -0
- package/src/model.ts +114 -130
- 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/renderToSvg.tsx +3 -0
- package/src/seqPosToGlobalCol.test.ts +39 -51
- package/src/seqPosToGlobalCol.ts +6 -41
- package/src/stripDefaultSnapshot.test.ts +11 -0
- package/src/useCanvasAutorun.test.tsx +98 -0
- package/src/useCanvasAutorun.ts +30 -11
- package/src/version.ts +1 -1
package/src/constants.ts
CHANGED
|
@@ -6,6 +6,7 @@ export const defaultScrollX = 0
|
|
|
6
6
|
export const defaultScrollY = 0
|
|
7
7
|
export const defaultCurrentAlignment = 0
|
|
8
8
|
export const defaultShowDomains = false
|
|
9
|
+
export const defaultShowDomainLegend = true
|
|
9
10
|
export const defaultHideGaps = true
|
|
10
11
|
export const defaultAllowedGappyness = 100
|
|
11
12
|
export const defaultSubFeatureRows = false
|
package/src/model.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
defaultScrollX,
|
|
32
32
|
defaultScrollY,
|
|
33
33
|
defaultScrollZoom,
|
|
34
|
+
defaultShowDomainLegend,
|
|
34
35
|
defaultShowDomains,
|
|
35
36
|
defaultSubFeatureRows,
|
|
36
37
|
maxCellSize,
|
|
@@ -132,6 +133,15 @@ function stateModelFactory() {
|
|
|
132
133
|
* #property
|
|
133
134
|
*/
|
|
134
135
|
showDomains: stripDefault(types.boolean, defaultShowDomains),
|
|
136
|
+
/**
|
|
137
|
+
* #property
|
|
138
|
+
* whether the domain legend is expanded. The legend floats over the
|
|
139
|
+
* top-right of the alignment, so on a tall panel it covers real
|
|
140
|
+
* residues -- persisting the state is what lets a reader collapse it
|
|
141
|
+
* and keep it collapsed, and what lets a session or a figure open with
|
|
142
|
+
* it already out of the way.
|
|
143
|
+
*/
|
|
144
|
+
showDomainLegend: stripDefault(types.boolean, defaultShowDomainLegend),
|
|
135
145
|
/**
|
|
136
146
|
* #property
|
|
137
147
|
*/
|
|
@@ -475,6 +485,13 @@ function stateModelFactory() {
|
|
|
475
485
|
setShowDomains(arg: boolean) {
|
|
476
486
|
self.showDomains = arg
|
|
477
487
|
},
|
|
488
|
+
/**
|
|
489
|
+
* #action
|
|
490
|
+
* expand or collapse the domain legend that floats over the alignment
|
|
491
|
+
*/
|
|
492
|
+
setShowDomainLegend(arg: boolean) {
|
|
493
|
+
self.showDomainLegend = arg
|
|
494
|
+
},
|
|
478
495
|
/**
|
|
479
496
|
* #action
|
|
480
497
|
*/
|
|
@@ -1618,8 +1635,8 @@ function stateModelFactory() {
|
|
|
1618
1635
|
return 0
|
|
1619
1636
|
}
|
|
1620
1637
|
const col = self.seqPosGlobalColIndex.get(rowName)?.[seqPos]
|
|
1621
|
-
// past the end of the ungapped sequence: 0 for the degenerate
|
|
1622
|
-
//
|
|
1638
|
+
// past the end of the ungapped sequence: 0 for the degenerate all-gap
|
|
1639
|
+
// row, otherwise one past the last column
|
|
1623
1640
|
return col ?? (seqPos === 0 ? 0 : seq.length)
|
|
1624
1641
|
},
|
|
1625
1642
|
|
|
@@ -2067,79 +2084,100 @@ function stateModelFactory() {
|
|
|
2067
2084
|
})
|
|
2068
2085
|
}
|
|
2069
2086
|
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2087
|
+
/**
|
|
2088
|
+
* Fetch a filehandle whenever it changes, and hand the text to
|
|
2089
|
+
* `onLoad`.
|
|
2090
|
+
*
|
|
2091
|
+
* Every loader carries a generation guard: the filehandle can change
|
|
2092
|
+
* mid-fetch (a second file picked while the first is still in flight),
|
|
2093
|
+
* and the slower earlier request must not clobber the data, status, or
|
|
2094
|
+
* loading flag belonging to the newer one.
|
|
2095
|
+
*
|
|
2096
|
+
* `clearFilehandle` serves two purposes for the loaders that pass it.
|
|
2097
|
+
* A local file has no URL to refetch from, so the handle is dropped
|
|
2098
|
+
* once its bytes are in the model; and a fetch the user cancels drops
|
|
2099
|
+
* it too, returning the view to the import form rather than leaving a
|
|
2100
|
+
* stuck spinner.
|
|
2101
|
+
*/
|
|
2102
|
+
const loadOnFilehandleChange = ({
|
|
2103
|
+
getFilehandle,
|
|
2104
|
+
onLoad,
|
|
2105
|
+
setLoading,
|
|
2106
|
+
clearFilehandle,
|
|
2107
|
+
}: {
|
|
2108
|
+
getFilehandle: () => FileLocationType | undefined
|
|
2109
|
+
onLoad: (text: string) => void
|
|
2110
|
+
setLoading?: (arg: boolean) => void
|
|
2111
|
+
clearFilehandle?: () => void
|
|
2112
|
+
}) => {
|
|
2113
|
+
let generation = 0
|
|
2114
|
+
addDisposer(
|
|
2115
|
+
self,
|
|
2116
|
+
autorun(async () => {
|
|
2117
|
+
const filehandle = getFilehandle()
|
|
2118
|
+
if (!filehandle) {
|
|
2119
|
+
return
|
|
2120
|
+
}
|
|
2121
|
+
const current = ++generation
|
|
2122
|
+
const isCurrent = () => current === generation
|
|
2080
2123
|
try {
|
|
2081
|
-
|
|
2124
|
+
setLoading?.(true)
|
|
2125
|
+
self.setError(undefined)
|
|
2082
2126
|
const text = await fetchTextWithProgress(
|
|
2083
|
-
openLocation(
|
|
2127
|
+
openLocation(filehandle),
|
|
2084
2128
|
status => {
|
|
2085
|
-
if (
|
|
2129
|
+
if (isCurrent()) {
|
|
2086
2130
|
self.setStatus(status)
|
|
2087
2131
|
}
|
|
2088
2132
|
},
|
|
2089
2133
|
)
|
|
2090
|
-
if (
|
|
2134
|
+
if (isCurrent()) {
|
|
2091
2135
|
transaction(() => {
|
|
2092
|
-
|
|
2093
|
-
if (
|
|
2094
|
-
|
|
2095
|
-
self.setTreeFilehandle(undefined)
|
|
2136
|
+
onLoad(text)
|
|
2137
|
+
if (filehandle.locationType === 'BlobLocation') {
|
|
2138
|
+
clearFilehandle?.()
|
|
2096
2139
|
}
|
|
2097
2140
|
})
|
|
2098
2141
|
}
|
|
2099
2142
|
} catch (e) {
|
|
2100
|
-
if (
|
|
2143
|
+
if (isCurrent()) {
|
|
2101
2144
|
if (isAbortError(e)) {
|
|
2102
|
-
|
|
2103
|
-
// returns to the import form instead of a stuck spinner
|
|
2104
|
-
self.setTreeFilehandle(undefined)
|
|
2145
|
+
clearFilehandle?.()
|
|
2105
2146
|
} else {
|
|
2106
2147
|
console.error(e)
|
|
2107
2148
|
self.setError(e)
|
|
2108
2149
|
}
|
|
2109
2150
|
}
|
|
2110
2151
|
} finally {
|
|
2111
|
-
if (
|
|
2112
|
-
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
}),
|
|
2117
|
-
)
|
|
2118
|
-
// autorun opens treeMetadataFilehandle
|
|
2119
|
-
addDisposer(
|
|
2120
|
-
self,
|
|
2121
|
-
autorun(async () => {
|
|
2122
|
-
const { treeMetadataFilehandle } = self
|
|
2123
|
-
if (treeMetadataFilehandle) {
|
|
2124
|
-
try {
|
|
2125
|
-
self.setTreeMetadata(
|
|
2126
|
-
await fetchTextWithProgress(
|
|
2127
|
-
openLocation(treeMetadataFilehandle),
|
|
2128
|
-
status => {
|
|
2129
|
-
self.setStatus(status)
|
|
2130
|
-
},
|
|
2131
|
-
),
|
|
2132
|
-
)
|
|
2133
|
-
} catch (e) {
|
|
2134
|
-
// ignore user cancel (isAbortError); treeMetadata is optional
|
|
2135
|
-
if (!isAbortError(e)) {
|
|
2136
|
-
console.error(e)
|
|
2137
|
-
self.setError(e)
|
|
2152
|
+
if (isCurrent()) {
|
|
2153
|
+
setLoading?.(false)
|
|
2138
2154
|
}
|
|
2139
2155
|
}
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2156
|
+
}),
|
|
2157
|
+
)
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
loadOnFilehandleChange({
|
|
2161
|
+
getFilehandle: () => self.treeFilehandle,
|
|
2162
|
+
onLoad: text => {
|
|
2163
|
+
self.setTree(text)
|
|
2164
|
+
},
|
|
2165
|
+
setLoading: arg => {
|
|
2166
|
+
self.setLoadingTree(arg)
|
|
2167
|
+
},
|
|
2168
|
+
clearFilehandle: () => {
|
|
2169
|
+
self.setTreeFilehandle(undefined)
|
|
2170
|
+
},
|
|
2171
|
+
})
|
|
2172
|
+
|
|
2173
|
+
// treeMetadata is decorative and has no import-form step of its own, so
|
|
2174
|
+
// it keeps no loading flag and nothing to return to on cancel
|
|
2175
|
+
loadOnFilehandleChange({
|
|
2176
|
+
getFilehandle: () => self.treeMetadataFilehandle,
|
|
2177
|
+
onLoad: text => {
|
|
2178
|
+
self.setTreeMetadata(text)
|
|
2179
|
+
},
|
|
2180
|
+
})
|
|
2143
2181
|
|
|
2144
2182
|
// autorun parses inline gff text from data.gff
|
|
2145
2183
|
addDisposer(
|
|
@@ -2157,83 +2195,29 @@ function stateModelFactory() {
|
|
|
2157
2195
|
}),
|
|
2158
2196
|
)
|
|
2159
2197
|
|
|
2160
|
-
//
|
|
2161
|
-
|
|
2162
|
-
self,
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
self.setStatus(status)
|
|
2171
|
-
},
|
|
2172
|
-
)
|
|
2173
|
-
self.applyGFFText(gffText)
|
|
2174
|
-
if (gffFilehandle.locationType === 'BlobLocation') {
|
|
2175
|
-
self.setGFFFilehandle(undefined)
|
|
2176
|
-
}
|
|
2177
|
-
} catch (e) {
|
|
2178
|
-
// on user cancel (isAbortError) drop the filehandle
|
|
2179
|
-
if (isAbortError(e)) {
|
|
2180
|
-
self.setGFFFilehandle(undefined)
|
|
2181
|
-
} else {
|
|
2182
|
-
console.error(e)
|
|
2183
|
-
self.setError(e)
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
}),
|
|
2188
|
-
)
|
|
2198
|
+
// gffFilehandle carries InterProScan domains
|
|
2199
|
+
loadOnFilehandleChange({
|
|
2200
|
+
getFilehandle: () => self.gffFilehandle,
|
|
2201
|
+
onLoad: text => {
|
|
2202
|
+
self.applyGFFText(text)
|
|
2203
|
+
},
|
|
2204
|
+
clearFilehandle: () => {
|
|
2205
|
+
self.setGFFFilehandle(undefined)
|
|
2206
|
+
},
|
|
2207
|
+
})
|
|
2189
2208
|
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
openLocation(msaFilehandle),
|
|
2203
|
-
status => {
|
|
2204
|
-
if (generation === msaGeneration) {
|
|
2205
|
-
self.setStatus(status)
|
|
2206
|
-
}
|
|
2207
|
-
},
|
|
2208
|
-
)
|
|
2209
|
-
if (generation === msaGeneration) {
|
|
2210
|
-
transaction(() => {
|
|
2211
|
-
self.setMSA(txt)
|
|
2212
|
-
if (msaFilehandle.locationType === 'BlobLocation') {
|
|
2213
|
-
// clear filehandle after loading if from a local file
|
|
2214
|
-
self.setMSAFilehandle(undefined)
|
|
2215
|
-
}
|
|
2216
|
-
})
|
|
2217
|
-
}
|
|
2218
|
-
} catch (e) {
|
|
2219
|
-
if (generation === msaGeneration) {
|
|
2220
|
-
if (isAbortError(e)) {
|
|
2221
|
-
// cancelled by the user: drop the filehandle so the view
|
|
2222
|
-
// returns to the import form instead of a stuck spinner
|
|
2223
|
-
self.setMSAFilehandle(undefined)
|
|
2224
|
-
} else {
|
|
2225
|
-
console.error(e)
|
|
2226
|
-
self.setError(e)
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
} finally {
|
|
2230
|
-
if (generation === msaGeneration) {
|
|
2231
|
-
self.setLoadingMSA(false)
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
}),
|
|
2236
|
-
)
|
|
2209
|
+
loadOnFilehandleChange({
|
|
2210
|
+
getFilehandle: () => self.msaFilehandle,
|
|
2211
|
+
onLoad: text => {
|
|
2212
|
+
self.setMSA(text)
|
|
2213
|
+
},
|
|
2214
|
+
setLoading: arg => {
|
|
2215
|
+
self.setLoadingMSA(arg)
|
|
2216
|
+
},
|
|
2217
|
+
clearFilehandle: () => {
|
|
2218
|
+
self.setMSAFilehandle(undefined)
|
|
2219
|
+
},
|
|
2220
|
+
})
|
|
2237
2221
|
|
|
2238
2222
|
// Keep the parse chain warm: reading self.columns transitively holds
|
|
2239
2223
|
// self.MSA (parseMSA) computed alive, so it is parsed once per data
|
|
@@ -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
|
|