@jbrowse/plugin-linear-genome-view 1.5.6 → 1.6.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/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.d.ts +35 -17
- package/dist/LinearBareDisplay/model.d.ts +28 -4
- package/dist/LinearBasicDisplay/model.d.ts +28 -4
- package/dist/LinearGenomeView/components/Header.d.ts +2 -1
- package/dist/LinearGenomeView/components/RefNameAutocomplete.d.ts +2 -1
- package/dist/LinearGenomeView/components/ScaleBar.d.ts +8 -8
- package/dist/LinearGenomeView/components/SearchBox.d.ts +8 -0
- package/dist/LinearGenomeView/index.d.ts +7 -4
- package/dist/index.d.ts +85 -14
- package/dist/plugin-linear-genome-view.cjs.development.js +738 -389
- package/dist/plugin-linear-genome-view.cjs.development.js.map +1 -1
- package/dist/plugin-linear-genome-view.cjs.production.min.js +1 -1
- package/dist/plugin-linear-genome-view.cjs.production.min.js.map +1 -1
- package/dist/plugin-linear-genome-view.esm.js +749 -401
- package/dist/plugin-linear-genome-view.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +308 -88
- package/src/BaseLinearDisplay/models/baseLinearDisplayConfigSchema.ts +10 -3
- package/src/LinearBasicDisplay/configSchema.ts +0 -6
- package/src/LinearGenomeView/components/Header.tsx +38 -120
- package/src/LinearGenomeView/components/ImportForm.tsx +1 -1
- package/src/LinearGenomeView/components/LinearGenomeView.test.js +6 -4
- package/src/LinearGenomeView/components/RefNameAutocomplete.tsx +9 -5
- package/src/LinearGenomeView/components/SearchBox.tsx +111 -0
- package/src/LinearGenomeView/components/TrackLabel.tsx +10 -6
- package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +10 -11
- package/src/LinearGenomeView/index.tsx +25 -53
- package/src/index.ts +61 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/plugin-linear-genome-view",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "JBrowse 2 linear genome view",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jbrowse",
|
|
@@ -61,5 +61,5 @@
|
|
|
61
61
|
"publishConfig": {
|
|
62
62
|
"access": "public"
|
|
63
63
|
},
|
|
64
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "403d855f184c88fad85f44e079dd6fffac276984"
|
|
65
65
|
}
|
|
@@ -1,28 +1,35 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { Button, Typography } from '@material-ui/core'
|
|
2
4
|
import { BaseDisplay } from '@jbrowse/core/pluggableElementTypes/models'
|
|
3
5
|
import { getConf } from '@jbrowse/core/configuration'
|
|
4
6
|
import { MenuItem } from '@jbrowse/core/ui'
|
|
5
7
|
import {
|
|
8
|
+
isAbortException,
|
|
6
9
|
getContainingView,
|
|
7
10
|
getSession,
|
|
8
11
|
isSelectionContainer,
|
|
9
12
|
isSessionModelWithWidgets,
|
|
10
13
|
} from '@jbrowse/core/util'
|
|
14
|
+
import { Stats } from '@jbrowse/core/data_adapters/BaseAdapter'
|
|
11
15
|
import { BaseBlock } from '@jbrowse/core/util/blockTypes'
|
|
12
16
|
import { Region } from '@jbrowse/core/util/types'
|
|
13
17
|
import CompositeMap from '@jbrowse/core/util/compositeMap'
|
|
14
18
|
import { Feature, isFeature } from '@jbrowse/core/util/simpleFeature'
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
import {
|
|
20
|
+
getParentRenderProps,
|
|
21
|
+
getRpcSessionId,
|
|
22
|
+
} from '@jbrowse/core/util/tracks'
|
|
19
23
|
import { autorun } from 'mobx'
|
|
20
|
-
import { addDisposer,
|
|
21
|
-
|
|
24
|
+
import { addDisposer, isAlive, types, Instance } from 'mobx-state-tree'
|
|
25
|
+
// icons
|
|
26
|
+
import MenuOpenIcon from '@material-ui/icons/MenuOpen'
|
|
27
|
+
|
|
28
|
+
import { LinearGenomeViewModel, ExportSvgOptions } from '../../LinearGenomeView'
|
|
22
29
|
import { Tooltip } from '../components/BaseLinearDisplay'
|
|
23
30
|
import BlockState, { renderBlockData } from './serverSideRenderedBlock'
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
type LGV = LinearGenomeViewModel
|
|
26
33
|
|
|
27
34
|
export interface Layout {
|
|
28
35
|
minX: number
|
|
@@ -31,10 +38,30 @@ export interface Layout {
|
|
|
31
38
|
maxY: number
|
|
32
39
|
name: string
|
|
33
40
|
}
|
|
41
|
+
|
|
42
|
+
// stabilize clipid under test for snapshot
|
|
43
|
+
function getId(id: string, index: number) {
|
|
44
|
+
const isJest = typeof jest === 'undefined'
|
|
45
|
+
return `clip-${isJest ? id : 'jest'}-${index}`
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
type LayoutRecord = [number, number, number, number]
|
|
35
49
|
|
|
50
|
+
function getDisplayStr(totalBytes: number) {
|
|
51
|
+
let displayBp
|
|
52
|
+
if (Math.floor(totalBytes / 1000000) > 0) {
|
|
53
|
+
displayBp = `${parseFloat((totalBytes / 1000000).toPrecision(3))} Mb`
|
|
54
|
+
} else if (Math.floor(totalBytes / 1000) > 0) {
|
|
55
|
+
displayBp = `${parseFloat((totalBytes / 1000).toPrecision(3))} Kb`
|
|
56
|
+
} else {
|
|
57
|
+
displayBp = `${Math.floor(totalBytes)} bytes`
|
|
58
|
+
}
|
|
59
|
+
return displayBp
|
|
60
|
+
}
|
|
61
|
+
|
|
36
62
|
const minDisplayHeight = 20
|
|
37
63
|
const defaultDisplayHeight = 100
|
|
64
|
+
|
|
38
65
|
export const BaseLinearDisplay = types
|
|
39
66
|
.compose(
|
|
40
67
|
'BaseLinearDisplay',
|
|
@@ -50,13 +77,17 @@ export const BaseLinearDisplay = types
|
|
|
50
77
|
),
|
|
51
78
|
blockState: types.map(BlockState),
|
|
52
79
|
userBpPerPxLimit: types.maybe(types.number),
|
|
80
|
+
userByteSizeLimit: types.maybe(types.number),
|
|
53
81
|
}),
|
|
54
82
|
)
|
|
55
83
|
.volatile(() => ({
|
|
84
|
+
currBpPerPx: 0,
|
|
56
85
|
message: '',
|
|
57
86
|
featureIdUnderMouse: undefined as undefined | string,
|
|
58
87
|
contextMenuFeature: undefined as undefined | Feature,
|
|
59
88
|
scrollTop: 0,
|
|
89
|
+
estimatedRegionStatsP: undefined as undefined | Promise<Stats>,
|
|
90
|
+
estimatedRegionStats: undefined as undefined | Stats,
|
|
60
91
|
}))
|
|
61
92
|
.views(self => ({
|
|
62
93
|
get blockType(): 'staticBlocks' | 'dynamicBlocks' {
|
|
@@ -64,7 +95,7 @@ export const BaseLinearDisplay = types
|
|
|
64
95
|
},
|
|
65
96
|
get blockDefinitions() {
|
|
66
97
|
const { blockType } = this
|
|
67
|
-
const view = getContainingView(self) as
|
|
98
|
+
const view = getContainingView(self) as LGV
|
|
68
99
|
if (!view.initialized) {
|
|
69
100
|
throw new Error('view not initialized yet')
|
|
70
101
|
}
|
|
@@ -72,13 +103,6 @@ export const BaseLinearDisplay = types
|
|
|
72
103
|
},
|
|
73
104
|
}))
|
|
74
105
|
.views(self => ({
|
|
75
|
-
/**
|
|
76
|
-
* set limit to config amount, or user amount if they force load,
|
|
77
|
-
*/
|
|
78
|
-
get maxViewBpPerPx() {
|
|
79
|
-
return self.userBpPerPxLimit || getConf(self, 'maxDisplayedBpPerPx')
|
|
80
|
-
},
|
|
81
|
-
|
|
82
106
|
/**
|
|
83
107
|
* how many milliseconds to wait for the display to
|
|
84
108
|
* "settle" before re-rendering a block
|
|
@@ -113,56 +137,83 @@ export const BaseLinearDisplay = types
|
|
|
113
137
|
return undefined as undefined | React.FC<any>
|
|
114
138
|
},
|
|
115
139
|
}))
|
|
116
|
-
.views(self => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
featureMaps.push(block.features)
|
|
127
|
-
}
|
|
140
|
+
.views(self => ({
|
|
141
|
+
/**
|
|
142
|
+
* a CompositeMap of `featureId -> feature obj` that
|
|
143
|
+
* just looks in all the block data for that feature
|
|
144
|
+
*/
|
|
145
|
+
get features() {
|
|
146
|
+
const featureMaps = []
|
|
147
|
+
for (const block of self.blockState.values()) {
|
|
148
|
+
if (block && block.features) {
|
|
149
|
+
featureMaps.push(block.features)
|
|
128
150
|
}
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
}
|
|
152
|
+
return new CompositeMap(featureMaps)
|
|
153
|
+
},
|
|
131
154
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
},
|
|
155
|
+
get featureUnderMouse() {
|
|
156
|
+
const feat = self.featureIdUnderMouse
|
|
157
|
+
return feat ? this.features.get(feat) : undefined
|
|
158
|
+
},
|
|
137
159
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
160
|
+
getFeatureOverlapping(blockKey: string, x: number, y: number) {
|
|
161
|
+
return self.blockState.get(blockKey)?.layout?.getByCoord(x, y)
|
|
162
|
+
},
|
|
141
163
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
164
|
+
getFeatureByID(blockKey: string, id: string): LayoutRecord | undefined {
|
|
165
|
+
return self.blockState.get(blockKey)?.layout?.getByID(id)
|
|
166
|
+
},
|
|
145
167
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
// if block key is not supplied, can look at all blocks
|
|
169
|
+
searchFeatureByID(id: string): LayoutRecord | undefined {
|
|
170
|
+
let ret
|
|
171
|
+
self.blockState.forEach(block => {
|
|
172
|
+
const val = block?.layout?.getByID(id)
|
|
173
|
+
if (val) {
|
|
174
|
+
ret = val
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
return ret
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
get currentBytesRequested() {
|
|
181
|
+
return self.estimatedRegionStats?.bytes || 0
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
get currentFeatureScreenDensity() {
|
|
185
|
+
const view = getContainingView(self) as LGV
|
|
186
|
+
return (self.estimatedRegionStats?.featureDensity || 0) * view.bpPerPx
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
get maxFeatureScreenDensity() {
|
|
190
|
+
return getConf(self, 'maxFeatureScreenDensity')
|
|
191
|
+
},
|
|
192
|
+
get estimatedStatsReady() {
|
|
193
|
+
return !!self.estimatedRegionStats
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
get maxAllowableBytes() {
|
|
197
|
+
return (
|
|
198
|
+
self.userByteSizeLimit ||
|
|
199
|
+
self.estimatedRegionStats?.fetchSizeLimit ||
|
|
200
|
+
(getConf(self, 'fetchSizeLimit') as number)
|
|
201
|
+
)
|
|
202
|
+
},
|
|
203
|
+
}))
|
|
159
204
|
.actions(self => ({
|
|
205
|
+
// base display reload does nothing, see specialized displays for details
|
|
206
|
+
setMessage(message: string) {
|
|
207
|
+
self.message = message
|
|
208
|
+
},
|
|
209
|
+
|
|
160
210
|
afterAttach() {
|
|
161
|
-
// watch the parent's blocks to update our block state when they change
|
|
211
|
+
// watch the parent's blocks to update our block state when they change,
|
|
212
|
+
// then we recreate the blocks on our own model (creating and deleting to
|
|
213
|
+
// match the parent blocks)
|
|
162
214
|
const blockWatchDisposer = autorun(() => {
|
|
163
|
-
// create any blocks that we need to create
|
|
164
215
|
const blocksPresent: { [key: string]: boolean } = {}
|
|
165
|
-
const view = getContainingView(self) as
|
|
216
|
+
const view = getContainingView(self) as LGV
|
|
166
217
|
if (view.initialized) {
|
|
167
218
|
self.blockDefinitions.contentBlocks.forEach(block => {
|
|
168
219
|
blocksPresent[block.key] = true
|
|
@@ -170,7 +221,6 @@ export const BaseLinearDisplay = types
|
|
|
170
221
|
this.addBlock(block.key, block)
|
|
171
222
|
}
|
|
172
223
|
})
|
|
173
|
-
// delete any blocks we need go delete
|
|
174
224
|
self.blockState.forEach((_, key) => {
|
|
175
225
|
if (!blocksPresent[key]) {
|
|
176
226
|
this.deleteBlock(key)
|
|
@@ -181,6 +231,54 @@ export const BaseLinearDisplay = types
|
|
|
181
231
|
|
|
182
232
|
addDisposer(self, blockWatchDisposer)
|
|
183
233
|
},
|
|
234
|
+
|
|
235
|
+
estimateRegionsStats(
|
|
236
|
+
regions: Region[],
|
|
237
|
+
opts: {
|
|
238
|
+
headers?: Record<string, string>
|
|
239
|
+
signal?: AbortSignal
|
|
240
|
+
filters?: string[]
|
|
241
|
+
},
|
|
242
|
+
) {
|
|
243
|
+
if (self.estimatedRegionStatsP) {
|
|
244
|
+
return self.estimatedRegionStatsP
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const { rpcManager } = getSession(self)
|
|
248
|
+
const { adapterConfig } = self
|
|
249
|
+
const sessionId = getRpcSessionId(self)
|
|
250
|
+
|
|
251
|
+
const params = {
|
|
252
|
+
sessionId,
|
|
253
|
+
regions,
|
|
254
|
+
adapterConfig,
|
|
255
|
+
statusCallback: (message: string) => {
|
|
256
|
+
if (isAlive(self)) {
|
|
257
|
+
this.setMessage(message)
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
...opts,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
self.estimatedRegionStatsP = rpcManager
|
|
264
|
+
.call(sessionId, 'CoreEstimateRegionStats', params)
|
|
265
|
+
.catch(e => {
|
|
266
|
+
this.setRegionStatsP(undefined)
|
|
267
|
+
throw e
|
|
268
|
+
}) as Promise<Stats>
|
|
269
|
+
|
|
270
|
+
return self.estimatedRegionStatsP
|
|
271
|
+
},
|
|
272
|
+
setRegionStatsP(p?: Promise<Stats>) {
|
|
273
|
+
self.estimatedRegionStatsP = p
|
|
274
|
+
},
|
|
275
|
+
setRegionStats(estimatedRegionStats?: Stats) {
|
|
276
|
+
self.estimatedRegionStats = estimatedRegionStats
|
|
277
|
+
},
|
|
278
|
+
clearRegionStats() {
|
|
279
|
+
self.estimatedRegionStatsP = undefined
|
|
280
|
+
self.estimatedRegionStats = undefined
|
|
281
|
+
},
|
|
184
282
|
setHeight(displayHeight: number) {
|
|
185
283
|
if (displayHeight > minDisplayHeight) {
|
|
186
284
|
self.height = displayHeight
|
|
@@ -194,17 +292,20 @@ export const BaseLinearDisplay = types
|
|
|
194
292
|
const newHeight = this.setHeight(self.height + distance)
|
|
195
293
|
return newHeight - oldHeight
|
|
196
294
|
},
|
|
295
|
+
|
|
197
296
|
setScrollTop(scrollTop: number) {
|
|
198
297
|
self.scrollTop = scrollTop
|
|
199
298
|
},
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
299
|
+
|
|
300
|
+
updateStatsLimit(stats: Stats) {
|
|
301
|
+
const view = getContainingView(self) as LGV
|
|
302
|
+
if (stats.bytes) {
|
|
303
|
+
self.userByteSizeLimit = stats.bytes
|
|
304
|
+
} else {
|
|
305
|
+
self.userBpPerPxLimit = view.bpPerPx
|
|
306
|
+
}
|
|
207
307
|
},
|
|
308
|
+
|
|
208
309
|
addBlock(key: string, block: BaseBlock) {
|
|
209
310
|
self.blockState.set(
|
|
210
311
|
key,
|
|
@@ -214,6 +315,9 @@ export const BaseLinearDisplay = types
|
|
|
214
315
|
}),
|
|
215
316
|
)
|
|
216
317
|
},
|
|
318
|
+
setCurrBpPerPx(n: number) {
|
|
319
|
+
self.currBpPerPx = n
|
|
320
|
+
},
|
|
217
321
|
deleteBlock(key: string) {
|
|
218
322
|
self.blockState.delete(key)
|
|
219
323
|
},
|
|
@@ -246,12 +350,123 @@ export const BaseLinearDisplay = types
|
|
|
246
350
|
},
|
|
247
351
|
}))
|
|
248
352
|
.views(self => ({
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
353
|
+
// region is too large if:
|
|
354
|
+
// - stats are ready
|
|
355
|
+
// - region is greater than 20kb (don't warn when zoomed in less than that)
|
|
356
|
+
// - and bytes > max allowed bytes || curr density>max density
|
|
357
|
+
get regionTooLarge() {
|
|
358
|
+
const view = getContainingView(self) as LGV
|
|
359
|
+
if (!self.estimatedStatsReady || view.dynamicBlocks.totalBp < 20_000) {
|
|
360
|
+
return false
|
|
253
361
|
}
|
|
254
|
-
|
|
362
|
+
const bpLimitOrDensity = self.userBpPerPxLimit
|
|
363
|
+
? view.bpPerPx > self.userBpPerPxLimit
|
|
364
|
+
: self.currentFeatureScreenDensity > self.maxFeatureScreenDensity
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
self.currentBytesRequested > self.maxAllowableBytes || bpLimitOrDensity
|
|
368
|
+
)
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
// only shows a message of bytes requested is defined, the feature density
|
|
372
|
+
// based stats don't produce any helpful message besides to zoom in
|
|
373
|
+
get regionTooLargeReason() {
|
|
374
|
+
const req = self.currentBytesRequested
|
|
375
|
+
const max = self.maxAllowableBytes
|
|
376
|
+
|
|
377
|
+
return req && req > max
|
|
378
|
+
? `Requested too much data (${getDisplayStr(req)})`
|
|
379
|
+
: ''
|
|
380
|
+
},
|
|
381
|
+
}))
|
|
382
|
+
.actions(self => {
|
|
383
|
+
const { reload: superReload } = self
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
async reload() {
|
|
387
|
+
self.setError()
|
|
388
|
+
const aborter = new AbortController()
|
|
389
|
+
const view = getContainingView(self) as LGV
|
|
390
|
+
if (!view.initialized) {
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
self.estimatedRegionStatsP = self.estimateRegionsStats(
|
|
396
|
+
view.staticBlocks.contentBlocks,
|
|
397
|
+
{ signal: aborter.signal },
|
|
398
|
+
)
|
|
399
|
+
const estimatedRegionStats = await self.estimatedRegionStatsP
|
|
400
|
+
|
|
401
|
+
if (isAlive(self)) {
|
|
402
|
+
self.setRegionStats(estimatedRegionStats)
|
|
403
|
+
superReload()
|
|
404
|
+
} else {
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
} catch (e) {
|
|
408
|
+
self.setError(e)
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
afterAttach() {
|
|
412
|
+
// this autorun performs stats estimation
|
|
413
|
+
//
|
|
414
|
+
// the chain of events calls estimateRegionStats against the data
|
|
415
|
+
// adapter which by default uses featureDensity, but can also respond
|
|
416
|
+
// with a byte size estimate and fetch size limit (data adapter can
|
|
417
|
+
// define what is too much data)
|
|
418
|
+
addDisposer(
|
|
419
|
+
self,
|
|
420
|
+
autorun(
|
|
421
|
+
async () => {
|
|
422
|
+
try {
|
|
423
|
+
const aborter = new AbortController()
|
|
424
|
+
const view = getContainingView(self) as LGV
|
|
425
|
+
|
|
426
|
+
if (!view.initialized) {
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// don't re-estimate featureDensity even if zoom level changes,
|
|
431
|
+
// jbrowse1-style assume it's sort of representative
|
|
432
|
+
if (self.estimatedRegionStats?.featureDensity !== undefined) {
|
|
433
|
+
self.setCurrBpPerPx(view.bpPerPx)
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// we estimate stats once at a given zoom level
|
|
438
|
+
if (view.bpPerPx === self.currBpPerPx) {
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
self.clearRegionStats()
|
|
443
|
+
self.setCurrBpPerPx(view.bpPerPx)
|
|
444
|
+
const statsP = self.estimateRegionsStats(
|
|
445
|
+
view.staticBlocks.contentBlocks,
|
|
446
|
+
{ signal: aborter.signal },
|
|
447
|
+
)
|
|
448
|
+
self.setRegionStatsP(statsP)
|
|
449
|
+
const estimatedRegionStats = await statsP
|
|
450
|
+
|
|
451
|
+
if (isAlive(self)) {
|
|
452
|
+
self.setRegionStats(estimatedRegionStats)
|
|
453
|
+
}
|
|
454
|
+
} catch (e) {
|
|
455
|
+
if (!isAbortException(e) && isAlive(self)) {
|
|
456
|
+
console.error(e)
|
|
457
|
+
self.setError(e)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
{ delay: 500 },
|
|
462
|
+
),
|
|
463
|
+
)
|
|
464
|
+
},
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
.views(self => ({
|
|
468
|
+
regionCannotBeRenderedText(_region: Region) {
|
|
469
|
+
return self.regionTooLarge ? 'Force load to see features' : ''
|
|
255
470
|
},
|
|
256
471
|
|
|
257
472
|
/**
|
|
@@ -262,18 +477,24 @@ export const BaseLinearDisplay = types
|
|
|
262
477
|
* react node allows user to force load at current setting
|
|
263
478
|
*/
|
|
264
479
|
regionCannotBeRendered(_region: Region) {
|
|
265
|
-
const
|
|
266
|
-
|
|
480
|
+
const { regionTooLarge, regionTooLargeReason } = self
|
|
481
|
+
|
|
482
|
+
if (regionTooLarge) {
|
|
267
483
|
return (
|
|
268
484
|
<>
|
|
269
485
|
<Typography component="span" variant="body2">
|
|
486
|
+
{regionTooLargeReason ? regionTooLargeReason + '. ' : ''}
|
|
270
487
|
Zoom in to see features or{' '}
|
|
271
488
|
</Typography>
|
|
272
489
|
<Button
|
|
273
|
-
data-testid="
|
|
490
|
+
data-testid="force_reload_button"
|
|
274
491
|
onClick={() => {
|
|
275
|
-
self.
|
|
276
|
-
|
|
492
|
+
if (!self.estimatedRegionStats) {
|
|
493
|
+
console.error('No global stats?')
|
|
494
|
+
} else {
|
|
495
|
+
self.updateStatsLimit(self.estimatedRegionStats)
|
|
496
|
+
self.reload()
|
|
497
|
+
}
|
|
277
498
|
}}
|
|
278
499
|
variant="outlined"
|
|
279
500
|
>
|
|
@@ -308,8 +529,11 @@ export const BaseLinearDisplay = types
|
|
|
308
529
|
: []
|
|
309
530
|
},
|
|
310
531
|
renderProps() {
|
|
532
|
+
const view = getContainingView(self) as LGV
|
|
311
533
|
return {
|
|
312
534
|
...getParentRenderProps(self),
|
|
535
|
+
notReady:
|
|
536
|
+
self.currBpPerPx !== view.bpPerPx || !self.estimatedRegionStats,
|
|
313
537
|
rpcDriverName: self.rpcDriverName,
|
|
314
538
|
displayModel: self,
|
|
315
539
|
onFeatureClick(_: unknown, featureId: string | undefined) {
|
|
@@ -318,7 +542,9 @@ export const BaseLinearDisplay = types
|
|
|
318
542
|
self.clearFeatureSelection()
|
|
319
543
|
} else {
|
|
320
544
|
const feature = self.features.get(f)
|
|
321
|
-
|
|
545
|
+
if (feature) {
|
|
546
|
+
self.selectFeature(feature)
|
|
547
|
+
}
|
|
322
548
|
}
|
|
323
549
|
},
|
|
324
550
|
onClick() {
|
|
@@ -354,15 +580,11 @@ export const BaseLinearDisplay = types
|
|
|
354
580
|
async renderSvg(opts: ExportSvgOptions & { overrideHeight: number }) {
|
|
355
581
|
const { height, id } = self
|
|
356
582
|
const { overrideHeight } = opts
|
|
357
|
-
const view = getContainingView(self) as
|
|
358
|
-
const {
|
|
359
|
-
offsetPx: viewOffsetPx,
|
|
360
|
-
roundedDynamicBlocks: dynamicBlocks,
|
|
361
|
-
width,
|
|
362
|
-
} = view
|
|
583
|
+
const view = getContainingView(self) as LGV
|
|
584
|
+
const { offsetPx: viewOffsetPx, roundedDynamicBlocks, width } = view
|
|
363
585
|
|
|
364
586
|
const renderings = await Promise.all(
|
|
365
|
-
|
|
587
|
+
roundedDynamicBlocks.map(block => {
|
|
366
588
|
const blockState = BlockState.create({
|
|
367
589
|
key: block.key,
|
|
368
590
|
region: block,
|
|
@@ -401,12 +623,9 @@ export const BaseLinearDisplay = types
|
|
|
401
623
|
return (
|
|
402
624
|
<>
|
|
403
625
|
{renderings.map((rendering, index) => {
|
|
404
|
-
const { offsetPx } =
|
|
626
|
+
const { offsetPx } = roundedDynamicBlocks[index]
|
|
405
627
|
const offset = offsetPx - viewOffsetPx
|
|
406
|
-
|
|
407
|
-
const clipid = `clip-${
|
|
408
|
-
typeof jest === 'undefined' ? id : 'jest'
|
|
409
|
-
}-${index}`
|
|
628
|
+
const clipid = getId(id, index)
|
|
410
629
|
return (
|
|
411
630
|
<React.Fragment key={`frag-${index}`}>
|
|
412
631
|
<defs>
|
|
@@ -424,6 +643,7 @@ export const BaseLinearDisplay = types
|
|
|
424
643
|
{React.isValidElement(rendering.reactElement) ? (
|
|
425
644
|
rendering.reactElement
|
|
426
645
|
) : (
|
|
646
|
+
// eslint-disable-next-line react/no-danger
|
|
427
647
|
<g dangerouslySetInnerHTML={{ __html: rendering.html }} />
|
|
428
648
|
)}
|
|
429
649
|
</g>
|
|
@@ -3,10 +3,17 @@ import { ConfigurationSchema } from '@jbrowse/core/configuration'
|
|
|
3
3
|
export const baseLinearDisplayConfigSchema = ConfigurationSchema(
|
|
4
4
|
'BaseLinearDisplay',
|
|
5
5
|
{
|
|
6
|
-
|
|
6
|
+
maxFeatureScreenDensity: {
|
|
7
7
|
type: 'number',
|
|
8
|
-
description:
|
|
9
|
-
|
|
8
|
+
description:
|
|
9
|
+
'maximum features per pixel that is displayed in the view, used if byte size estimates not available',
|
|
10
|
+
defaultValue: 0.3,
|
|
11
|
+
},
|
|
12
|
+
fetchSizeLimit: {
|
|
13
|
+
type: 'number',
|
|
14
|
+
defaultValue: 1_000_000,
|
|
15
|
+
description:
|
|
16
|
+
"maximum data to attempt to download for a given track, used if adapter doesn't specify one",
|
|
10
17
|
},
|
|
11
18
|
},
|
|
12
19
|
{ explicitIdentifier: 'displayId' },
|
|
@@ -14,12 +14,6 @@ export default function configSchemaFactory(pluginManager: PluginManager) {
|
|
|
14
14
|
contextVariable: ['feature'],
|
|
15
15
|
},
|
|
16
16
|
renderer: pluginManager.pluggableConfigSchemaType('renderer'),
|
|
17
|
-
// overrides base
|
|
18
|
-
maxDisplayedBpPerPx: {
|
|
19
|
-
type: 'number',
|
|
20
|
-
description: 'maximum bpPerPx that is displayed in the view',
|
|
21
|
-
defaultValue: 1000,
|
|
22
|
-
},
|
|
23
17
|
},
|
|
24
18
|
{ baseConfiguration: baseLinearDisplayConfigSchema, explicitlyTyped: true },
|
|
25
19
|
)
|