@apollo-annotation/jbrowse-plugin-apollo 0.3.3 → 0.3.4

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.
@@ -11,6 +11,39 @@ import { ApolloSessionModel } from '../../session'
11
11
  import { baseModelFactory } from './base'
12
12
  import { boxGlyph, geneGlyph, genericChildGlyph } from '../glyphs'
13
13
 
14
+ function getRowsForFeature(
15
+ startingRow: number,
16
+ rowCount: number,
17
+ filledRowLocations: Map<number, [number, number][]>,
18
+ ) {
19
+ const rowsForFeature = []
20
+ for (let i = startingRow; i < startingRow + rowCount; i++) {
21
+ const row = filledRowLocations.get(i)
22
+ if (row) {
23
+ rowsForFeature.push(row)
24
+ }
25
+ }
26
+ return rowsForFeature
27
+ }
28
+
29
+ function canPlaceFeatureInRows(
30
+ rowsForFeature: [number, number][][],
31
+ feature: AnnotationFeature,
32
+ ) {
33
+ for (const rowForFeature of rowsForFeature) {
34
+ for (const [rowStart, rowEnd] of rowForFeature) {
35
+ if (
36
+ doesIntersect2(feature.min, feature.max, rowStart, rowEnd) ||
37
+ doesIntersect2(rowStart, rowEnd, feature.min, feature.max)
38
+ ) {
39
+ return false
40
+ }
41
+ }
42
+ }
43
+
44
+ return true
45
+ }
46
+
14
47
  export function layoutsModelFactory(
15
48
  pluginManager: PluginManager,
16
49
  configSchema: AnyConfigurationSchemaType,
@@ -19,48 +52,14 @@ export function layoutsModelFactory(
19
52
 
20
53
  return BaseLinearApolloDisplay.named('LinearApolloDisplayLayouts')
21
54
  .props({
22
- featuresMinMaxLimit: 500_000,
55
+ cleanupBoundary: 200_000,
23
56
  })
24
57
  .volatile(() => ({
25
58
  seenFeatures: observable.map<string, AnnotationFeature>(),
26
59
  }))
27
60
  .views((self) => ({
28
- get featuresMinMax() {
29
- const { assemblyManager } =
30
- self.session as unknown as AbstractSessionModel
31
- return self.lgv.displayedRegions.map((region) => {
32
- const assembly = assemblyManager.get(region.assemblyName)
33
- let min: number | undefined
34
- let max: number | undefined
35
- const { end, refName, start } = region
36
- for (const [, feature] of self.seenFeatures) {
37
- if (
38
- refName !== assembly?.getCanonicalRefName(feature.refSeq) ||
39
- !doesIntersect2(start, end, feature.min, feature.max) ||
40
- feature.length > self.featuresMinMaxLimit ||
41
- (self.filteredFeatureTypes.length > 0 &&
42
- !self.filteredFeatureTypes.includes(feature.type))
43
- ) {
44
- continue
45
- }
46
- if (min === undefined) {
47
- ;({ min } = feature)
48
- }
49
- if (max === undefined) {
50
- ;({ max } = feature)
51
- }
52
- if (feature.minWithChildren < min) {
53
- ;({ min } = feature)
54
- }
55
- if (feature.maxWithChildren > max) {
56
- ;({ max } = feature)
57
- }
58
- }
59
- if (min !== undefined && max !== undefined) {
60
- return [min, max]
61
- }
62
- return
63
- })
61
+ getAnnotationFeatureById(id: string) {
62
+ return self.seenFeatures.get(id)
64
63
  },
65
64
  getGlyph(feature: AnnotationFeature) {
66
65
  if (this.looksLikeGene(feature)) {
@@ -91,15 +90,9 @@ export function layoutsModelFactory(
91
90
  if (!grandChildren?.size) {
92
91
  return false
93
92
  }
94
- const hasCDS = [...grandChildren.values()].some((grandchild) =>
95
- featureTypeOntology.isTypeOf(grandchild.type, 'CDS'),
96
- )
97
- const hasExon = [...grandChildren.values()].some((grandchild) =>
93
+ return [...grandChildren.values()].some((grandchild) =>
98
94
  featureTypeOntology.isTypeOf(grandchild.type, 'exon'),
99
95
  )
100
- if (hasCDS && hasExon) {
101
- return true
102
- }
103
96
  }
104
97
  }
105
98
  return false
@@ -117,15 +110,11 @@ export function layoutsModelFactory(
117
110
  get featureLayouts() {
118
111
  const { assemblyManager } =
119
112
  self.session as unknown as AbstractSessionModel
120
- return self.lgv.displayedRegions.map((region, idx) => {
113
+ return self.lgv.displayedRegions.map((region) => {
121
114
  const assembly = assemblyManager.get(region.assemblyName)
122
- const featureLayout = new Map<number, [number, AnnotationFeature][]>()
123
- const minMax = self.featuresMinMax[idx]
124
- if (!minMax) {
125
- return featureLayout
126
- }
127
- const [min, max] = minMax
128
- const rows: boolean[][] = []
115
+ const featureLayout = new Map<number, [number, string][]>()
116
+ // Track the occupied coordinates in each row
117
+ const filledRowLocations = new Map<number, [number, number][]>()
129
118
  const { end, refName, start } = region
130
119
  for (const [id, feature] of self.seenFeatures.entries()) {
131
120
  if (!isAlive(feature)) {
@@ -151,36 +140,24 @@ export function layoutsModelFactory(
151
140
  let startingRow = 0
152
141
  let placed = false
153
142
  while (!placed) {
154
- let rowsForFeature = rows.slice(
143
+ let rowsForFeature = getRowsForFeature(
155
144
  startingRow,
156
- startingRow + rowCount,
145
+ rowCount,
146
+ filledRowLocations,
157
147
  )
158
148
  if (rowsForFeature.length < rowCount) {
159
149
  for (let i = 0; i < rowCount - rowsForFeature.length; i++) {
160
- const newRowNumber = rows.length
161
- rows[newRowNumber] = Array.from({ length: max - min })
150
+ const newRowNumber = filledRowLocations.size
151
+ filledRowLocations.set(newRowNumber, [])
162
152
  featureLayout.set(newRowNumber, [])
163
153
  }
164
- rowsForFeature = rows.slice(startingRow, startingRow + rowCount)
154
+ rowsForFeature = getRowsForFeature(
155
+ startingRow,
156
+ rowCount,
157
+ filledRowLocations,
158
+ )
165
159
  }
166
- if (
167
- rowsForFeature
168
- .map((rowForFeature) => {
169
- // zero-length features are allowed in the spec
170
- const featureMax =
171
- feature.max - feature.min === 0
172
- ? feature.min + 1
173
- : feature.max
174
- let start = feature.min - min,
175
- end = featureMax - min
176
- if (feature.min - min < 0) {
177
- start = 0
178
- end = featureMax - feature.min
179
- }
180
- return rowForFeature.slice(start, end).some(Boolean)
181
- })
182
- .some(Boolean)
183
- ) {
160
+ if (!canPlaceFeatureInRows(rowsForFeature, feature)) {
184
161
  startingRow += 1
185
162
  continue
186
163
  }
@@ -189,16 +166,9 @@ export function layoutsModelFactory(
189
166
  rowNum < startingRow + rowCount;
190
167
  rowNum++
191
168
  ) {
192
- const row = rows[rowNum]
193
- let start = feature.min - min,
194
- end = feature.max - min
195
- if (feature.min - min < 0) {
196
- start = 0
197
- end = feature.max - feature.min
198
- }
199
- row.fill(true, start, end)
169
+ filledRowLocations.get(rowNum)?.push([feature.min, feature.max])
200
170
  const layoutRow = featureLayout.get(rowNum)
201
- layoutRow?.push([rowNum - startingRow, feature])
171
+ layoutRow?.push([rowNum - startingRow, feature._id])
202
172
  }
203
173
  placed = true
204
174
  }
@@ -212,12 +182,17 @@ export function layoutsModelFactory(
212
182
  self.session.apolloDataStore.ontologyManager
213
183
  for (const [idx, layout] of featureLayouts.entries()) {
214
184
  for (const [layoutRowNum, layoutRow] of layout) {
215
- for (const [featureRowNum, layoutFeature] of layoutRow) {
185
+ for (const [featureRowNum, layoutFeatureId] of layoutRow) {
216
186
  if (featureRowNum !== 0) {
217
187
  // Same top-level feature in all feature rows, so only need to
218
188
  // check the first one
219
189
  continue
220
190
  }
191
+ const layoutFeature =
192
+ self.getAnnotationFeatureById(layoutFeatureId)
193
+ if (!layoutFeature) {
194
+ continue
195
+ }
221
196
  if (feature._id === layoutFeature._id) {
222
197
  return {
223
198
  layoutIndex: idx,
@@ -263,6 +238,30 @@ export function layoutsModelFactory(
263
238
  if (!self.lgv.initialized || self.regionCannotBeRendered()) {
264
239
  return
265
240
  }
241
+ // Clear out features that are no longer in the view and out of the cleanup boundary
242
+ // cleanup boundary + region boundary + cleanup boundary
243
+ for (const [id, feature] of self.seenFeatures.entries()) {
244
+ let shouldKeep = false
245
+ for (const region of self.regions) {
246
+ const extendedStart = region.start - self.cleanupBoundary
247
+ const extendedEnd = region.end + self.cleanupBoundary
248
+ if (
249
+ doesIntersect2(
250
+ extendedStart,
251
+ extendedEnd,
252
+ feature.min,
253
+ feature.max,
254
+ )
255
+ ) {
256
+ shouldKeep = true
257
+ break
258
+ }
259
+ }
260
+ if (!shouldKeep) {
261
+ self.deleteSeenFeature(id)
262
+ }
263
+ }
264
+ // Add features that are in the current view
266
265
  for (const region of self.regions) {
267
266
  const assembly = (
268
267
  self.session as unknown as ApolloSessionModel
@@ -139,13 +139,18 @@ export function mouseEventsModelIntermediateFactory(
139
139
  if (!layoutRow) {
140
140
  return mousePosition
141
141
  }
142
- const foundFeature = layoutRow.find(
143
- (f) => bp >= f[1].min && bp <= f[1].max,
144
- )
142
+ const foundFeature = layoutRow.find((f) => {
143
+ const feature = self.getAnnotationFeatureById(f[1])
144
+ return feature && bp >= feature.min && bp <= feature.max
145
+ })
145
146
  if (!foundFeature) {
146
147
  return mousePosition
147
148
  }
148
- const [featureRow, topLevelFeature] = foundFeature
149
+ const [featureRow, topLevelFeatureId] = foundFeature
150
+ const topLevelFeature = self.getAnnotationFeatureById(topLevelFeatureId)
151
+ if (!topLevelFeature) {
152
+ return mousePosition
153
+ }
149
154
  const glyph = self.getGlyph(topLevelFeature)
150
155
  const { featureTypeOntology } =
151
156
  self.session.apolloDataStore.ontologyManager
@@ -413,8 +413,9 @@ export function renderingModelFactory(
413
413
  for (const [idx, featureLayout] of featureLayouts.entries()) {
414
414
  const displayedRegion = displayedRegions[idx]
415
415
  for (const [row, featureLayoutRow] of featureLayout.entries()) {
416
- for (const [featureRow, feature] of featureLayoutRow) {
417
- if (featureRow > 0) {
416
+ for (const [featureRow, featureId] of featureLayoutRow) {
417
+ const feature = self.getAnnotationFeatureById(featureId)
418
+ if (featureRow > 0 || !feature) {
418
419
  continue
419
420
  }
420
421
  if (
@@ -35,6 +35,7 @@ export const OntologyRecordType = types
35
35
  })
36
36
  .volatile((_self) => ({
37
37
  dataStore: undefined as undefined | OntologyStore,
38
+ startedEquivalentTypeRequests: new Set<string>(),
38
39
  }))
39
40
  .actions((self) => ({
40
41
  /** does nothing, just used to access the model to force its lifecycle hooks to run */
@@ -66,6 +67,10 @@ export const OntologyRecordType = types
66
67
  if (!self.dataStore) {
67
68
  return
68
69
  }
70
+ if (self.startedEquivalentTypeRequests.has(type)) {
71
+ return
72
+ }
73
+ self.startedEquivalentTypeRequests.add(type)
69
74
  const terms = (yield self.dataStore.getTermsWithLabelOrSynonym(
70
75
  type,
71
76
  )) as unknown as OntologyTerm[]
@@ -75,6 +80,23 @@ export const OntologyRecordType = types
75
80
  self.setEquivalentTypes(type, equivalents)
76
81
  }),
77
82
  }))
83
+ .actions((self) => ({
84
+ afterCreate() {
85
+ autorun((reaction) => {
86
+ if (!self.dataStore) {
87
+ return
88
+ }
89
+ void self.loadEquivalentTypes('gene')
90
+ void self.loadEquivalentTypes('transcript')
91
+ void self.loadEquivalentTypes('CDS')
92
+ void self.loadEquivalentTypes('mRNA')
93
+ reaction.dispose()
94
+ })
95
+ },
96
+ setEquivalentTypes(type: string, equivalentTypes: string[]) {
97
+ self.equivalentTypes.set(type, equivalentTypes)
98
+ },
99
+ }))
78
100
  .views((self) => ({
79
101
  isTypeOf(queryType: string, typeOf: string): boolean {
80
102
  if (queryType === typeOf) {