@jbrowse/plugin-linear-genome-view 1.5.9 → 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.
Files changed (28) hide show
  1. package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.d.ts +35 -17
  2. package/dist/LinearBareDisplay/model.d.ts +28 -4
  3. package/dist/LinearBasicDisplay/model.d.ts +28 -4
  4. package/dist/LinearGenomeView/components/Header.d.ts +2 -1
  5. package/dist/LinearGenomeView/components/RefNameAutocomplete.d.ts +2 -1
  6. package/dist/LinearGenomeView/components/ScaleBar.d.ts +8 -8
  7. package/dist/LinearGenomeView/components/SearchBox.d.ts +8 -0
  8. package/dist/LinearGenomeView/index.d.ts +7 -4
  9. package/dist/index.d.ts +85 -14
  10. package/dist/plugin-linear-genome-view.cjs.development.js +738 -389
  11. package/dist/plugin-linear-genome-view.cjs.development.js.map +1 -1
  12. package/dist/plugin-linear-genome-view.cjs.production.min.js +1 -1
  13. package/dist/plugin-linear-genome-view.cjs.production.min.js.map +1 -1
  14. package/dist/plugin-linear-genome-view.esm.js +749 -401
  15. package/dist/plugin-linear-genome-view.esm.js.map +1 -1
  16. package/package.json +2 -2
  17. package/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +308 -88
  18. package/src/BaseLinearDisplay/models/baseLinearDisplayConfigSchema.ts +10 -3
  19. package/src/LinearBasicDisplay/configSchema.ts +0 -6
  20. package/src/LinearGenomeView/components/Header.tsx +38 -120
  21. package/src/LinearGenomeView/components/ImportForm.tsx +1 -1
  22. package/src/LinearGenomeView/components/LinearGenomeView.test.js +6 -4
  23. package/src/LinearGenomeView/components/RefNameAutocomplete.tsx +9 -5
  24. package/src/LinearGenomeView/components/SearchBox.tsx +111 -0
  25. package/src/LinearGenomeView/components/TrackLabel.tsx +10 -6
  26. package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +10 -11
  27. package/src/LinearGenomeView/index.tsx +25 -53
  28. 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.5.9",
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": "ed95a546c760209df8d59ff499dc81a6438d0f3e"
64
+ "gitHead": "403d855f184c88fad85f44e079dd6fffac276984"
65
65
  }
@@ -1,28 +1,35 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any,react/no-danger */
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 { getParentRenderProps } from '@jbrowse/core/util/tracks'
16
- import Button from '@material-ui/core/Button'
17
- import Typography from '@material-ui/core/Typography'
18
- import MenuOpenIcon from '@material-ui/icons/MenuOpen'
19
+ import {
20
+ getParentRenderProps,
21
+ getRpcSessionId,
22
+ } from '@jbrowse/core/util/tracks'
19
23
  import { autorun } from 'mobx'
20
- import { addDisposer, Instance, isAlive, types } from 'mobx-state-tree'
21
- import React from 'react'
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
- import { LinearGenomeViewModel, ExportSvgOptions } from '../../LinearGenomeView'
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 LinearGenomeViewModel
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
- return {
118
- /**
119
- * a CompositeMap of `featureId -> feature obj` that
120
- * just looks in all the block data for that feature
121
- */
122
- get features() {
123
- const featureMaps = []
124
- for (const block of self.blockState.values()) {
125
- if (block && block.features) {
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
- return new CompositeMap<string, Feature>(featureMaps)
130
- },
151
+ }
152
+ return new CompositeMap(featureMaps)
153
+ },
131
154
 
132
- get featureUnderMouse() {
133
- return self.featureIdUnderMouse
134
- ? this.features.get(self.featureIdUnderMouse)
135
- : undefined
136
- },
155
+ get featureUnderMouse() {
156
+ const feat = self.featureIdUnderMouse
157
+ return feat ? this.features.get(feat) : undefined
158
+ },
137
159
 
138
- getFeatureOverlapping(blockKey: string, x: number, y: number) {
139
- return self.blockState.get(blockKey)?.layout?.getByCoord(x, y)
140
- },
160
+ getFeatureOverlapping(blockKey: string, x: number, y: number) {
161
+ return self.blockState.get(blockKey)?.layout?.getByCoord(x, y)
162
+ },
141
163
 
142
- getFeatureByID(blockKey: string, id: string): LayoutRecord | undefined {
143
- return self.blockState.get(blockKey)?.layout?.getByID(id)
144
- },
164
+ getFeatureByID(blockKey: string, id: string): LayoutRecord | undefined {
165
+ return self.blockState.get(blockKey)?.layout?.getByID(id)
166
+ },
145
167
 
146
- // if block key is not supplied, can look at all blocks
147
- searchFeatureByID(id: string): LayoutRecord | undefined {
148
- let ret
149
- self.blockState.forEach(block => {
150
- const val = block?.layout?.getByID(id)
151
- if (val) {
152
- ret = val
153
- }
154
- })
155
- return ret
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 LinearGenomeViewModel
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
- // sets the new bpPerPxLimit if user chooses to force load
201
- setUserBpPerPxLimit(limit: number) {
202
- self.userBpPerPxLimit = limit
203
- },
204
- // base display reload does nothing, see specialized displays for details
205
- setMessage(message: string) {
206
- self.message = message
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
- regionCannotBeRenderedText(_region: Region) {
250
- const view = getContainingView(self) as LinearGenomeViewModel
251
- if (view && view.bpPerPx > self.maxViewBpPerPx) {
252
- return 'Zoom in to see features'
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
- return ''
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 view = getContainingView(self) as LinearGenomeViewModel
266
- if (view && view.bpPerPx > self.maxViewBpPerPx) {
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="reload_button"
490
+ data-testid="force_reload_button"
274
491
  onClick={() => {
275
- self.setUserBpPerPxLimit(view.bpPerPx)
276
- self.reload()
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
- self.selectFeature(feature as Feature)
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 LinearGenomeViewModel
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
- dynamicBlocks.map(block => {
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 } = dynamicBlocks[index]
626
+ const { offsetPx } = roundedDynamicBlocks[index]
405
627
  const offset = offsetPx - viewOffsetPx
406
- // stabalize clipid under test for snapshot
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
- maxDisplayedBpPerPx: {
6
+ maxFeatureScreenDensity: {
7
7
  type: 'number',
8
- description: 'maximum bpPerPx that is displayed in the view',
9
- defaultValue: Number.MAX_VALUE,
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
  )