jbrowse-plugin-mafviewer 1.1.4 → 1.2.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 (62) hide show
  1. package/dist/BgzipTaffyAdapter/BgzipTaffyAdapter.d.ts +5 -4
  2. package/dist/BgzipTaffyAdapter/BgzipTaffyAdapter.js +103 -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 +74 -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 +2 -2
  47. package/src/BgzipTaffyAdapter/BgzipTaffyAdapter.ts +123 -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 +211 -0
  59. package/src/LinearMafRenderer/util.ts +28 -1
  60. package/src/MafAddTrackWorkflow/AddTrackWorkflow.tsx +2 -1
  61. package/src/MafTabixAdapter/MafTabixAdapter.ts +92 -55
  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.0",
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",
@@ -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'
@@ -11,6 +19,7 @@ import { normalize } from '../util'
11
19
  import { parseRowInstructions } from './rowInstructions'
12
20
 
13
21
  import type { IndexData, OrganismRecord } from './types'
22
+ import { parseLineByLine } from './util'
14
23
  interface Entry {
15
24
  type: string
16
25
  row: number
@@ -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,110 @@ 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 || {}
104
+ console.log(query.start, query.end, 'wtf')
85
105
  return ObservableCreate<Feature>(async observer => {
86
106
  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: '',
107
+ const byteRanges = await this.setup()
108
+ const buffer = await updateStatus(
109
+ 'Downloading alignments',
110
+ statusCallback,
111
+ () => this.getLines(query, byteRanges),
112
+ )
113
+ if (buffer) {
114
+ const alignments = {} as Record<string, OrganismRecord>
115
+ const data = [] as Entry[]
116
+ let a0: any
117
+ let j = 0
118
+ let b = 0
119
+ parseLineByLine(buffer, line => {
120
+ if (j++ % 100 === 0) {
121
+ statusCallback(
122
+ `Processing ${toP(b / 1_000_000)}/${toP(buffer.length / 1_000_000)}Mb`,
123
+ )
124
+ }
125
+ b += line.length
126
+ if (line) {
127
+ const [lineData, rowInstructions] = line.split(' ; ')
128
+ if (rowInstructions) {
129
+ for (const ins of parseRowInstructions(rowInstructions)) {
130
+ if (ins.type === 'i') {
131
+ data.splice(ins.row, 0, ins)
132
+ if (!alignments[ins.asm]) {
133
+ alignments[ins.asm] = {
134
+ start: ins.start,
135
+ strand: ins.strand,
136
+ srcSize: ins.length,
137
+ chr: ins.ref,
138
+ data: '',
139
+ }
108
140
  }
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: '',
141
+ const e = alignments[ins.asm]!
142
+ e.data += ' '.repeat(Math.max(0, j - e.data.length)) // catch it up
143
+ } else if (ins.type === 's') {
144
+ if (!alignments[ins.asm]) {
145
+ alignments[ins.asm] = {
146
+ start: ins.start,
147
+ strand: ins.strand,
148
+ srcSize: ins.length,
149
+ chr: ins.ref,
150
+ data: '',
151
+ }
120
152
  }
153
+ const e = alignments[ins.asm]!
154
+ e.data += ' '.repeat(Math.max(0, j - e.data.length)) // catch it up
155
+ data[ins.row] = ins
156
+ } else if (ins.type === 'd') {
157
+ data.splice(ins.row, 1)
121
158
  }
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
159
 
129
- // no gaps for now(?)
130
- // else if (ins.type === 'g') {
131
- // }
132
- // else if (ins.type === 'G') {
133
- // }
160
+ // no gaps for now(?)
161
+ // else if (ins.type === 'g') {
162
+ // }
163
+ // else if (ins.type === 'G') {
164
+ // }
165
+ }
166
+ if (!a0) {
167
+ a0 = data[0]
168
+ }
134
169
  }
135
- if (!a0) {
136
- a0 = data[0]
170
+ const lineLen = lineData!.length
171
+ for (let i = 0; i < lineLen; i++) {
172
+ const letter = lineData![i]
173
+ const r = data[i]
174
+ if (r) {
175
+ alignments[r.asm]!.data += letter
176
+ } else {
177
+ // not sure why but chr22_KI270731v1_random.taf.gz ends up here
178
+ }
137
179
  }
138
180
  }
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
- }
181
+ })
182
+ if (a0) {
183
+ const row0 = alignments[a0.asm]!
184
+
185
+ // see
186
+ // https://github.com/ComparativeGenomicsToolkit/taffy/blob/f5a5354/docs/taffy_utilities.md#referenced-based-maftaf-and-indexing
187
+ // for the significance of row[0]:
188
+ //
189
+ // "An anchor line in TAF is a column from which all sequence
190
+ // coordinates can be deduced without scanning backwards to previous
191
+ // lines "
192
+ observer.next(
193
+ new SimpleFeature({
194
+ uniqueId: `${row0.start}-${row0.data.length}`,
195
+ refName: query.refName,
196
+ start: row0.start,
197
+ end: row0.start + row0.data.length,
198
+ strand: row0.strand,
199
+ alignments,
200
+ seq: row0.data,
201
+ }),
202
+ )
145
203
  }
146
204
  }
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
- }
205
+ statusCallback('')
169
206
  observer.complete()
170
207
  } catch (e) {
171
208
  observer.error(e)
@@ -188,15 +225,14 @@ export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
188
225
  }
189
226
  }
190
227
 
191
- async getLines(query: Region) {
192
- const byteRanges = await this.setup()
228
+ // TODO: cache processed large chunks
229
+ async getLines(query: Region, byteRanges: IndexData) {
193
230
  const file = openLocation(this.getConf('tafGzLocation'))
194
-
195
- const decoder = new TextDecoder('utf8')
196
231
  const records = byteRanges[query.refName]
197
232
  if (records) {
198
- let firstEntry = records[0]
233
+ let firstEntry
199
234
  let nextEntry
235
+
200
236
  // two pass:
201
237
  // first pass: find first block greater than query start, then -1 from
202
238
  // that
@@ -216,6 +252,9 @@ export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
216
252
  }
217
253
 
218
254
  nextEntry = nextEntry ?? records.at(-1)
255
+ // we NEED at least a firstEntry (validate behavior?) because othrwise it fetches whole
256
+ // file whn you request e.g. out of range region (e.g. taf in chr22:1-100
257
+ // and you are at chr22:200-300)
219
258
  if (firstEntry && nextEntry) {
220
259
  const response = await file.read(
221
260
  nextEntry.virtualOffset.blockPosition -
@@ -223,12 +262,10 @@ export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
223
262
  firstEntry.virtualOffset.blockPosition,
224
263
  )
225
264
  const buffer = await unzip(response)
226
- return decoder
227
- .decode(buffer.slice(firstEntry.virtualOffset.dataPosition))
228
- .split('\n')
265
+ return buffer.slice(firstEntry.virtualOffset.dataPosition)
229
266
  }
230
267
  }
231
- return []
268
+ return undefined
232
269
  }
233
270
 
234
271
  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'
13
6
  import { autorun } from 'mobx'
14
- import { Instance, addDisposer, isAlive, types } from 'mobx-state-tree'
7
+ import { addDisposer, isAlive, types } from 'mobx-state-tree'
8
+ import deepEqual from 'fast-deep-equal'
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,