jbrowse-plugin-mafviewer 1.1.4 → 1.2.1

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 (62) hide show
  1. package/dist/BgzipTaffyAdapter/BgzipTaffyAdapter.d.ts +5 -4
  2. package/dist/BgzipTaffyAdapter/BgzipTaffyAdapter.js +102 -82
  3. package/dist/BgzipTaffyAdapter/BgzipTaffyAdapter.js.map +1 -1
  4. package/dist/BgzipTaffyAdapter/util.d.ts +1 -0
  5. package/dist/BgzipTaffyAdapter/util.js +22 -0
  6. package/dist/BgzipTaffyAdapter/util.js.map +1 -0
  7. package/dist/BigMafAdapter/BigMafAdapter.d.ts +3 -2
  8. package/dist/BigMafAdapter/BigMafAdapter.js +4 -3
  9. package/dist/BigMafAdapter/BigMafAdapter.js.map +1 -1
  10. package/dist/LinearMafDisplay/components/ColorLegend.js +1 -1
  11. package/dist/LinearMafDisplay/components/ColorLegend.js.map +1 -1
  12. package/dist/LinearMafDisplay/components/ReactComponent.js +1 -1
  13. package/dist/LinearMafDisplay/components/ReactComponent.js.map +1 -1
  14. package/dist/LinearMafDisplay/components/SvgWrapper.js +2 -2
  15. package/dist/LinearMafDisplay/components/SvgWrapper.js.map +1 -1
  16. package/dist/LinearMafDisplay/components/YScaleBars.js +2 -2
  17. package/dist/LinearMafDisplay/components/YScaleBars.js.map +1 -1
  18. package/dist/LinearMafDisplay/renderSvg.d.ts +2 -2
  19. package/dist/LinearMafDisplay/renderSvg.js +0 -1
  20. package/dist/LinearMafDisplay/renderSvg.js.map +1 -1
  21. package/dist/LinearMafDisplay/stateModel.d.ts +19 -17
  22. package/dist/LinearMafDisplay/stateModel.js +17 -10
  23. package/dist/LinearMafDisplay/stateModel.js.map +1 -1
  24. package/dist/LinearMafRenderer/LinearMafRenderer.d.ts +8 -4
  25. package/dist/LinearMafRenderer/LinearMafRenderer.js +14 -148
  26. package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
  27. package/dist/LinearMafRenderer/configSchema.d.ts +6 -1
  28. package/dist/LinearMafRenderer/configSchema.js +6 -1
  29. package/dist/LinearMafRenderer/configSchema.js.map +1 -1
  30. package/dist/LinearMafRenderer/makeImageData.d.ts +20 -0
  31. package/dist/LinearMafRenderer/makeImageData.js +144 -0
  32. package/dist/LinearMafRenderer/makeImageData.js.map +1 -0
  33. package/dist/LinearMafRenderer/util.d.ts +6 -1
  34. package/dist/LinearMafRenderer/util.js +17 -0
  35. package/dist/LinearMafRenderer/util.js.map +1 -1
  36. package/dist/MafAddTrackWorkflow/AddTrackWorkflow.d.ts +1 -1
  37. package/dist/MafAddTrackWorkflow/AddTrackWorkflow.js.map +1 -1
  38. package/dist/MafTabixAdapter/MafTabixAdapter.d.ts +9 -6
  39. package/dist/MafTabixAdapter/MafTabixAdapter.js +69 -56
  40. package/dist/MafTabixAdapter/MafTabixAdapter.js.map +1 -1
  41. package/dist/MafTabixAdapter/configSchema.d.ts +7 -0
  42. package/dist/MafTabixAdapter/configSchema.js +7 -0
  43. package/dist/MafTabixAdapter/configSchema.js.map +1 -1
  44. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +7 -8
  45. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
  46. package/package.json +3 -2
  47. package/src/BgzipTaffyAdapter/BgzipTaffyAdapter.ts +122 -86
  48. package/src/BgzipTaffyAdapter/util.ts +25 -0
  49. package/src/BigMafAdapter/BigMafAdapter.ts +10 -4
  50. package/src/LinearMafDisplay/components/ColorLegend.tsx +1 -2
  51. package/src/LinearMafDisplay/components/ReactComponent.tsx +1 -1
  52. package/src/LinearMafDisplay/components/SvgWrapper.tsx +2 -2
  53. package/src/LinearMafDisplay/components/YScaleBars.tsx +3 -2
  54. package/src/LinearMafDisplay/renderSvg.tsx +5 -5
  55. package/src/LinearMafDisplay/stateModel.ts +30 -24
  56. package/src/LinearMafRenderer/LinearMafRenderer.ts +21 -176
  57. package/src/LinearMafRenderer/configSchema.ts +6 -1
  58. package/src/LinearMafRenderer/makeImageData.ts +210 -0
  59. package/src/LinearMafRenderer/util.ts +29 -1
  60. package/src/MafAddTrackWorkflow/AddTrackWorkflow.tsx +2 -1
  61. package/src/MafTabixAdapter/MafTabixAdapter.ts +84 -57
  62. package/src/MafTabixAdapter/configSchema.ts +7 -0
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.4",
2
+ "version": "1.2.1",
3
3
  "license": "MIT",
4
4
  "name": "jbrowse-plugin-mafviewer",
5
5
  "keywords": [
@@ -41,7 +41,7 @@
41
41
  "@typescript-eslint/eslint-plugin": "^8.18.0",
42
42
  "@typescript-eslint/parser": "^8.18.0",
43
43
  "chalk": "^5.3.0",
44
- "esbuild": "^0.24.0",
44
+ "esbuild": "^0.25.0",
45
45
  "eslint": "^9.17.0",
46
46
  "eslint-plugin-import": "^2.31.0",
47
47
  "eslint-plugin-react": "^7.20.3",
@@ -66,6 +66,7 @@
66
66
  "buffer": "^6.0.3",
67
67
  "d3-array": "^3.2.4",
68
68
  "d3-hierarchy": "^3.1.2",
69
+ "fast-deep-equal": "^3.1.3",
69
70
  "generic-filehandle2": "^1.0.0",
70
71
  "long": "^5.2.3"
71
72
  }
@@ -1,6 +1,14 @@
1
1
  import { unzip } from '@gmod/bgzf-filehandle'
2
- import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter'
3
- import { Feature, Region, SimpleFeature } from '@jbrowse/core/util'
2
+ import {
3
+ BaseFeatureDataAdapter,
4
+ BaseOptions,
5
+ } from '@jbrowse/core/data_adapters/BaseAdapter'
6
+ import {
7
+ Feature,
8
+ Region,
9
+ SimpleFeature,
10
+ updateStatus,
11
+ } from '@jbrowse/core/util'
4
12
  import { openLocation } from '@jbrowse/core/util/io'
5
13
  import { ObservableCreate } from '@jbrowse/core/util/rxjs'
6
14
  import Long from 'long'
@@ -9,6 +17,7 @@ import VirtualOffset from './virtualOffset'
9
17
  import parseNewick from '../parseNewick'
10
18
  import { normalize } from '../util'
11
19
  import { parseRowInstructions } from './rowInstructions'
20
+ import { parseLineByLine } from './util'
12
21
 
13
22
  import type { IndexData, OrganismRecord } from './types'
14
23
  interface Entry {
@@ -20,6 +29,9 @@ interface Entry {
20
29
  strand: number
21
30
  length: number
22
31
  }
32
+
33
+ const toP = (s = 0) => +(+s).toFixed(1)
34
+
23
35
  export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
24
36
  public setupP?: Promise<IndexData>
25
37
 
@@ -28,7 +40,7 @@ export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
28
40
  return Object.keys(data)
29
41
  }
30
42
 
31
- setup() {
43
+ setupPre() {
32
44
  if (!this.setupP) {
33
45
  this.setupP = this.readTaiFile().catch((e: unknown) => {
34
46
  this.setupP = undefined
@@ -37,6 +49,12 @@ export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
37
49
  }
38
50
  return this.setupP
39
51
  }
52
+ setup(opts?: BaseOptions) {
53
+ const { statusCallback = () => {} } = opts || {}
54
+ return updateStatus('Downloading index', statusCallback, () =>
55
+ this.setupPre(),
56
+ )
57
+ }
40
58
 
41
59
  async readTaiFile() {
42
60
  const text = await openLocation(this.getConf('taiLocation')).readFile(
@@ -81,91 +99,109 @@ export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
81
99
  return entries
82
100
  }
83
101
 
84
- getFeatures(query: Region) {
102
+ getFeatures(query: Region, opts?: BaseOptions) {
103
+ const { statusCallback = () => {} } = opts || {}
85
104
  return ObservableCreate<Feature>(async observer => {
86
105
  try {
87
- const lines = await this.getLines(query)
88
- const alignments = {} as Record<string, OrganismRecord>
89
-
90
- const k = lines.length
91
- const data = [] as Entry[]
92
- let a0: any
93
- for (let j = 0; j < k; j++) {
94
- const line = lines[j]!
95
- if (line) {
96
- const [lineData, rowInstructions] = line.split(' ; ')
97
- if (rowInstructions) {
98
- for (const ins of parseRowInstructions(rowInstructions)) {
99
- if (ins.type === 'i') {
100
- data.splice(ins.row, 0, ins)
101
- if (!alignments[ins.asm]) {
102
- alignments[ins.asm] = {
103
- start: ins.start,
104
- strand: ins.strand,
105
- srcSize: ins.length,
106
- chr: ins.ref,
107
- data: '',
106
+ const byteRanges = await this.setup()
107
+ const buffer = await updateStatus(
108
+ 'Downloading alignments',
109
+ statusCallback,
110
+ () => this.getLines(query, byteRanges),
111
+ )
112
+ if (buffer) {
113
+ const alignments = {} as Record<string, OrganismRecord>
114
+ const data = [] as Entry[]
115
+ let a0: any
116
+ let j = 0
117
+ let b = 0
118
+ parseLineByLine(buffer, line => {
119
+ if (j++ % 100 === 0) {
120
+ statusCallback(
121
+ `Processing ${toP(b / 1_000_000)}/${toP(buffer.length / 1_000_000)}Mb`,
122
+ )
123
+ }
124
+ b += line.length
125
+ if (line) {
126
+ const [lineData, rowInstructions] = line.split(' ; ')
127
+ if (rowInstructions) {
128
+ for (const ins of parseRowInstructions(rowInstructions)) {
129
+ if (ins.type === 'i') {
130
+ data.splice(ins.row, 0, ins)
131
+ if (!alignments[ins.asm]) {
132
+ alignments[ins.asm] = {
133
+ start: ins.start,
134
+ strand: ins.strand,
135
+ srcSize: ins.length,
136
+ chr: ins.ref,
137
+ data: '',
138
+ }
108
139
  }
109
- }
110
- const e = alignments[ins.asm]!
111
- e.data += ' '.repeat(Math.max(0, j - e.data.length)) // catch it up
112
- } else if (ins.type === 's') {
113
- if (!alignments[ins.asm]) {
114
- alignments[ins.asm] = {
115
- start: ins.start,
116
- strand: ins.strand,
117
- srcSize: ins.length,
118
- chr: ins.ref,
119
- data: '',
140
+ const e = alignments[ins.asm]!
141
+ e.data += ' '.repeat(Math.max(0, j - e.data.length)) // catch it up
142
+ } else if (ins.type === 's') {
143
+ if (!alignments[ins.asm]) {
144
+ alignments[ins.asm] = {
145
+ start: ins.start,
146
+ strand: ins.strand,
147
+ srcSize: ins.length,
148
+ chr: ins.ref,
149
+ data: '',
150
+ }
120
151
  }
152
+ const e = alignments[ins.asm]!
153
+ e.data += ' '.repeat(Math.max(0, j - e.data.length)) // catch it up
154
+ data[ins.row] = ins
155
+ } else if (ins.type === 'd') {
156
+ data.splice(ins.row, 1)
121
157
  }
122
- const e = alignments[ins.asm]!
123
- e.data += ' '.repeat(Math.max(0, j - e.data.length)) // catch it up
124
- data[ins.row] = ins
125
- } else if (ins.type === 'd') {
126
- data.splice(ins.row, 1)
127
- }
128
158
 
129
- // no gaps for now(?)
130
- // else if (ins.type === 'g') {
131
- // }
132
- // else if (ins.type === 'G') {
133
- // }
159
+ // no gaps for now(?)
160
+ // else if (ins.type === 'g') {
161
+ // }
162
+ // else if (ins.type === 'G') {
163
+ // }
164
+ }
165
+ if (!a0) {
166
+ a0 = data[0]
167
+ }
134
168
  }
135
- if (!a0) {
136
- a0 = data[0]
169
+ const lineLen = lineData!.length
170
+ for (let i = 0; i < lineLen; i++) {
171
+ const letter = lineData![i]
172
+ const r = data[i]
173
+ if (r) {
174
+ alignments[r.asm]!.data += letter
175
+ } else {
176
+ // not sure why but chr22_KI270731v1_random.taf.gz ends up here
177
+ }
137
178
  }
138
179
  }
139
- const lineLen = lineData!.length
140
- for (let i = 0; i < lineLen; i++) {
141
- const letter = lineData![i]
142
- const r = data[i]!
143
- alignments[r.asm]!.data += letter
144
- }
180
+ })
181
+ if (a0) {
182
+ const row0 = alignments[a0.asm]!
183
+
184
+ // see
185
+ // https://github.com/ComparativeGenomicsToolkit/taffy/blob/f5a5354/docs/taffy_utilities.md#referenced-based-maftaf-and-indexing
186
+ // for the significance of row[0]:
187
+ //
188
+ // "An anchor line in TAF is a column from which all sequence
189
+ // coordinates can be deduced without scanning backwards to previous
190
+ // lines "
191
+ observer.next(
192
+ new SimpleFeature({
193
+ uniqueId: `${row0.start}-${row0.data.length}`,
194
+ refName: query.refName,
195
+ start: row0.start,
196
+ end: row0.start + row0.data.length,
197
+ strand: row0.strand,
198
+ alignments,
199
+ seq: row0.data,
200
+ }),
201
+ )
145
202
  }
146
203
  }
147
- if (a0) {
148
- const row0 = alignments[a0.asm]!
149
-
150
- // see
151
- // https://github.com/ComparativeGenomicsToolkit/taffy/blob/f5a5354/docs/taffy_utilities.md#referenced-based-maftaf-and-indexing
152
- // for the significance of row[0]:
153
- //
154
- // "An anchor line in TAF is a column from which all sequence
155
- // coordinates can be deduced without scanning backwards to previous
156
- // lines "
157
- observer.next(
158
- new SimpleFeature({
159
- uniqueId: `${row0.start}-${row0.data.length}`,
160
- refName: query.refName,
161
- start: row0.start,
162
- end: row0.start + row0.data.length,
163
- strand: row0.strand,
164
- alignments,
165
- seq: row0.data,
166
- }),
167
- )
168
- }
204
+ statusCallback('')
169
205
  observer.complete()
170
206
  } catch (e) {
171
207
  observer.error(e)
@@ -188,15 +224,14 @@ export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
188
224
  }
189
225
  }
190
226
 
191
- async getLines(query: Region) {
192
- const byteRanges = await this.setup()
227
+ // TODO: cache processed large chunks
228
+ async getLines(query: Region, byteRanges: IndexData) {
193
229
  const file = openLocation(this.getConf('tafGzLocation'))
194
-
195
- const decoder = new TextDecoder('utf8')
196
230
  const records = byteRanges[query.refName]
197
231
  if (records) {
198
- let firstEntry = records[0]
232
+ let firstEntry
199
233
  let nextEntry
234
+
200
235
  // two pass:
201
236
  // first pass: find first block greater than query start, then -1 from
202
237
  // that
@@ -216,6 +251,9 @@ export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
216
251
  }
217
252
 
218
253
  nextEntry = nextEntry ?? records.at(-1)
254
+ // we NEED at least a firstEntry (validate behavior?) because othrwise it fetches whole
255
+ // file whn you request e.g. out of range region (e.g. taf in chr22:1-100
256
+ // and you are at chr22:200-300)
219
257
  if (firstEntry && nextEntry) {
220
258
  const response = await file.read(
221
259
  nextEntry.virtualOffset.blockPosition -
@@ -223,12 +261,10 @@ export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
223
261
  firstEntry.virtualOffset.blockPosition,
224
262
  )
225
263
  const buffer = await unzip(response)
226
- return decoder
227
- .decode(buffer.slice(firstEntry.virtualOffset.dataPosition))
228
- .split('\n')
264
+ return buffer.slice(firstEntry.virtualOffset.dataPosition)
229
265
  }
230
266
  }
231
- return []
267
+ return undefined
232
268
  }
233
269
 
234
270
  freeResources(): void {}
@@ -0,0 +1,25 @@
1
+ export function parseLineByLine<T>(
2
+ buffer: Uint8Array,
3
+ cb: (line: string) => T | undefined,
4
+ ): T[] {
5
+ let blockStart = 0
6
+ const entries: T[] = []
7
+ const decoder = new TextDecoder('utf8')
8
+ while (blockStart < buffer.length) {
9
+ const n = buffer.indexOf(10, blockStart)
10
+ if (n === -1) {
11
+ break
12
+ }
13
+ const b = buffer.subarray(blockStart, n)
14
+ const line = decoder.decode(b).trim()
15
+ if (line) {
16
+ const entry = cb(line)
17
+ if (entry) {
18
+ entries.push(entry)
19
+ }
20
+ }
21
+
22
+ blockStart = n + 1
23
+ }
24
+ return entries
25
+ }
@@ -1,5 +1,5 @@
1
1
  import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter'
2
- import { Feature, Region, SimpleFeature } from '@jbrowse/core/util'
2
+ import { SimpleFeature, updateStatus } from '@jbrowse/core/util'
3
3
  import { openLocation } from '@jbrowse/core/util/io'
4
4
  import { ObservableCreate } from '@jbrowse/core/util/rxjs'
5
5
  import { getSnapshot } from 'mobx-state-tree'
@@ -8,6 +8,9 @@ import { firstValueFrom, toArray } from 'rxjs'
8
8
  import parseNewick from '../parseNewick'
9
9
  import { normalize } from '../util'
10
10
 
11
+ import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter'
12
+ import type { Feature, Region } from '@jbrowse/core/util'
13
+
11
14
  interface OrganismRecord {
12
15
  chr: string
13
16
  start: number
@@ -51,11 +54,14 @@ export default class BigMafAdapter extends BaseFeatureDataAdapter {
51
54
  return adapter.getHeader()
52
55
  }
53
56
 
54
- getFeatures(query: Region) {
57
+ getFeatures(query: Region, opts?: BaseOptions) {
58
+ const { statusCallback = () => {} } = opts || {}
55
59
  return ObservableCreate<Feature>(async observer => {
56
60
  const { adapter } = await this.setup()
57
- const features = await firstValueFrom(
58
- adapter.getFeatures(query).pipe(toArray()),
61
+ const features = await updateStatus(
62
+ 'Downloading alignment',
63
+ statusCallback,
64
+ () => firstValueFrom(adapter.getFeatures(query).pipe(toArray())),
59
65
  )
60
66
  for (const feature of features) {
61
67
  const maf = feature.get('mafBlock') as string
@@ -2,7 +2,6 @@ import React from 'react'
2
2
 
3
3
  import { observer } from 'mobx-react'
4
4
 
5
- // locals
6
5
  import { LinearMafDisplayModel } from '../stateModel'
7
6
  import RectBg from './RectBg'
8
7
  import Tree from './Tree'
@@ -16,7 +15,7 @@ const ColorLegend = observer(function ({
16
15
  svgFontSize: number
17
16
  labelWidth: number
18
17
  }) {
19
- const { totalHeight, treeWidth, samples, rowHeight } = model
18
+ const { totalHeight, treeWidth, samples = [], rowHeight } = model
20
19
  const canDisplayLabel = rowHeight >= 8
21
20
  const boxHeight = Math.min(20, rowHeight)
22
21
 
@@ -52,7 +52,7 @@ const LinearMafDisplay = observer(function (props: {
52
52
  >
53
53
  <BaseLinearDisplayComponent {...props} />
54
54
  <YScaleBars model={model} />
55
- {mouseY ? (
55
+ {mouseY && sources ? (
56
56
  <div style={{ position: 'relative' }}>
57
57
  <svg
58
58
  className={classes.cursor}
@@ -18,7 +18,7 @@ const SvgWrapper = observer(function ({
18
18
  if (exportSVG) {
19
19
  return <>{children}</>
20
20
  } else {
21
- const { rowHeight, samples } = model
21
+ const { totalHeight } = model
22
22
  return (
23
23
  <svg
24
24
  style={{
@@ -26,7 +26,7 @@ const SvgWrapper = observer(function ({
26
26
  top: 0,
27
27
  left: 0,
28
28
  pointerEvents: 'none',
29
- height: samples.length * rowHeight,
29
+ height: totalHeight,
30
30
  width: getContainingView(model).width,
31
31
  }}
32
32
  >
@@ -22,8 +22,9 @@ export const YScaleBars = observer(function (props: {
22
22
 
23
23
  const labelWidth = max(
24
24
  samples
25
- .map(s => measureText(s.label, svgFontSize))
26
- .map(width => (canDisplayLabel ? width : minWidth)),
25
+ ?.map(s => measureText(s.label, svgFontSize))
26
+ .map(width => (canDisplayLabel ? width : minWidth)) || [],
27
+ 0,
27
28
  )
28
29
 
29
30
  return (
@@ -1,15 +1,15 @@
1
1
  import React from 'react'
2
2
 
3
3
  import { getContainingView } from '@jbrowse/core/util'
4
- import {
4
+
5
+ import YScaleBars from './components/YScaleBars'
6
+
7
+ import type { LinearMafDisplayModel } from './stateModel'
8
+ import type {
5
9
  ExportSvgDisplayOptions,
6
10
  LinearGenomeViewModel,
7
11
  } from '@jbrowse/plugin-linear-genome-view'
8
12
 
9
- // locals
10
- import YScaleBars from './components/YScaleBars'
11
- import { LinearMafDisplayModel } from './stateModel'
12
-
13
13
  export async function renderSvg(
14
14
  self: LinearMafDisplayModel,
15
15
  opts: ExportSvgDisplayOptions,
@@ -1,27 +1,26 @@
1
- import PluginManager from '@jbrowse/core/PluginManager'
2
- import {
3
- AnyConfigurationModel,
4
- AnyConfigurationSchemaType,
5
- ConfigurationReference,
6
- getConf,
7
- } from '@jbrowse/core/configuration'
1
+ import { ConfigurationReference, getConf } from '@jbrowse/core/configuration'
8
2
  import { getEnv, getSession } from '@jbrowse/core/util'
9
3
  import { getRpcSessionId } from '@jbrowse/core/util/tracks'
10
- import { ExportSvgDisplayOptions } from '@jbrowse/plugin-linear-genome-view'
11
4
  import { ascending } from 'd3-array'
12
- import { type HierarchyNode, cluster, hierarchy } from 'd3-hierarchy'
5
+ import { cluster, hierarchy } from 'd3-hierarchy'
6
+ import deepEqual from 'fast-deep-equal'
13
7
  import { autorun } from 'mobx'
14
- import { Instance, addDisposer, isAlive, types } from 'mobx-state-tree'
8
+ import { addDisposer, isAlive, types } from 'mobx-state-tree'
15
9
 
16
10
  import SetRowHeightDialog from './components/SetRowHeight'
17
- import {
18
- NodeWithIds,
19
- NodeWithIdsAndLength,
20
- maxLength,
21
- setBrLength,
22
- } from './types'
11
+ import { maxLength, setBrLength } from './types'
23
12
  import { normalize } from '../util'
24
13
 
14
+ import type { NodeWithIds, NodeWithIdsAndLength } from './types'
15
+ import type PluginManager from '@jbrowse/core/PluginManager'
16
+ import type {
17
+ AnyConfigurationModel,
18
+ AnyConfigurationSchemaType,
19
+ } from '@jbrowse/core/configuration'
20
+ import type { ExportSvgDisplayOptions } from '@jbrowse/plugin-linear-genome-view'
21
+ import type { HierarchyNode } from 'd3-hierarchy'
22
+ import type { Instance } from 'mobx-state-tree'
23
+
25
24
  interface Sample {
26
25
  id: string
27
26
  label: string
@@ -90,11 +89,11 @@ export default function stateModelFactory(
90
89
  /**
91
90
  * #volatile
92
91
  */
93
- volatileSamples: [] as Sample[],
92
+ volatileSamples: undefined as Sample[] | undefined,
94
93
  /**
95
94
  * #volatile
96
95
  */
97
- tree: undefined as any,
96
+ volatileTree: undefined as any,
98
97
  }))
99
98
  .actions(self => ({
100
99
  /**
@@ -125,8 +124,12 @@ export default function stateModelFactory(
125
124
  * #action
126
125
  */
127
126
  setSamples({ samples, tree }: { samples: Sample[]; tree: unknown }) {
128
- self.volatileSamples = samples
129
- self.tree = tree
127
+ if (!deepEqual(samples, self.volatileSamples)) {
128
+ self.volatileSamples = samples
129
+ }
130
+ if (!deepEqual(tree, self.volatileTree)) {
131
+ self.volatileTree = tree
132
+ }
130
133
  },
131
134
  }))
132
135
  .views(self => ({
@@ -159,8 +162,8 @@ export default function stateModelFactory(
159
162
  * #getter
160
163
  */
161
164
  get root() {
162
- return self.tree
163
- ? hierarchy(self.tree, d => d.children)
165
+ return self.volatileTree
166
+ ? hierarchy(self.volatileTree, d => d.children)
164
167
  // todo: investigate whether needed, typescript says children always true
165
168
  .sum(d => (d.children ? 0 : 1))
166
169
  .sort((a, b) => ascending(a.data.length || 1, b.data.length || 1))
@@ -196,7 +199,7 @@ export default function stateModelFactory(
196
199
  * #getter
197
200
  */
198
201
  get totalHeight() {
199
- return this.samples.length * self.rowHeight
202
+ return this.samples ? this.samples.length * self.rowHeight : 1
200
203
  },
201
204
  /**
202
205
  * #getter
@@ -237,8 +240,11 @@ export default function stateModelFactory(
237
240
  rowProportion,
238
241
  mismatchRendering,
239
242
  } = self
243
+ const s = superRenderProps()
240
244
  return {
241
- ...superRenderProps(),
245
+ ...s,
246
+ notReady:
247
+ !self.volatileSamples || !self.volatileTree || super.notReady,
242
248
  config: rendererConfig,
243
249
  samples,
244
250
  rowHeight,