Skip to content

Commit 646c390

Browse files
Improve gene glyph display and performance (GMOD#523)
* Remove unused unserializable state Fixes the following error when opening the TranscriptDetailsWidget: DataCloneError: Failed to execute 'put' on 'IDBObjectStore': [object Object] could not be cloned. * Pre-load commonly checked equivalent types * apollo track updates * feature layout improvements and loading indicator * clean up old features * fixes * fixes * add circular loader * Move loading circle a bit to avoid scrollbar skip-checks:true --------- Co-authored-by: Shashank Budhanuru Ramaraju <shashank.b.r.gowda45587@gmail.com>
1 parent 7e50341 commit 646c390

File tree

7 files changed

+152
-88
lines changed

7 files changed

+152
-88
lines changed

packages/jbrowse-plugin-apollo/src/FeatureDetailsWidget/model.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { ElementId } from '@jbrowse/core/util/types/mst'
99
import { autorun } from 'mobx'
1010
import { Instance, SnapshotIn, addDisposer, types } from 'mobx-state-tree'
1111

12-
import { ChangeManager } from '../ChangeManager'
1312
import { ApolloSessionModel } from '../session'
1413

1514
export const ApolloFeatureDetailsWidgetModel = types
@@ -86,7 +85,6 @@ export const ApolloTranscriptDetailsModel = types
8685
),
8786
assembly: types.string,
8887
refName: types.string,
89-
changeManager: types.frozen<ChangeManager>(),
9088
})
9189
.volatile(() => ({
9290
tryReload: undefined as string | undefined,

packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ import {
99
} from '@jbrowse/core/util'
1010
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
1111
import ErrorIcon from '@mui/icons-material/Error'
12-
import { Alert, Avatar, Tooltip, useTheme } from '@mui/material'
12+
import {
13+
Alert,
14+
Avatar,
15+
CircularProgress,
16+
Tooltip,
17+
useTheme,
18+
} from '@mui/material'
1319
import { observer } from 'mobx-react'
1420
import React, { useEffect, useState } from 'react'
1521
import { makeStyles } from 'tss-react/mui'
@@ -39,6 +45,13 @@ const useStyles = makeStyles()((theme) => ({
3945
color: theme.palette.warning.light,
4046
backgroundColor: theme.palette.warning.contrastText,
4147
},
48+
loading: {
49+
position: 'absolute',
50+
right: theme.spacing(3),
51+
zIndex: 10,
52+
pointerEvents: 'none',
53+
textAlign: 'right',
54+
},
4255
}))
4356

4457
export const LinearApolloDisplay = observer(function LinearApolloDisplay(
@@ -47,6 +60,7 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
4760
const theme = useTheme()
4861
const { model } = props
4962
const {
63+
loading,
5064
apolloRowHeight,
5165
contextMenuItems: getContextMenuItems,
5266
cursor,
@@ -128,6 +142,11 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
128142
}
129143
}}
130144
>
145+
{loading ? (
146+
<div className={classes.loading}>
147+
<CircularProgress size="18px" />
148+
</div>
149+
) : null}
131150
{message ? (
132151
<Alert severity="warning" classes={{ message: classes.ellipses }}>
133152
<Tooltip title={message}>

packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/base.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export function baseModelFactory(
4444
),
4545
),
4646
filteredFeatureTypes: types.array(types.string),
47+
loadingState: false,
4748
})
4849
.views((self) => {
4950
const { configuration, renderProps: superRenderProps } = self
@@ -76,6 +77,9 @@ export function baseModelFactory(
7677
}
7778
return 300
7879
},
80+
get loading() {
81+
return self.loadingState
82+
},
7983
}))
8084
.views((self) => ({
8185
get rendererTypeName() {
@@ -167,6 +171,9 @@ export function baseModelFactory(
167171
updateFilteredFeatureTypes(types: string[]) {
168172
self.filteredFeatureTypes = cast(types)
169173
},
174+
setLoading(loading: boolean) {
175+
self.loadingState = loading
176+
},
170177
}))
171178
.views((self) => {
172179
const { filteredFeatureTypes, trackMenuItems: superTrackMenuItems } = self
@@ -244,9 +251,16 @@ export function baseModelFactory(
244251
if (!self.lgv.initialized || self.regionCannotBeRendered()) {
245252
return
246253
}
254+
self.setLoading(true)
247255
void (
248256
self.session as unknown as ApolloSessionModel
249-
).apolloDataStore.loadFeatures(self.regions)
257+
).apolloDataStore
258+
.loadFeatures(self.regions)
259+
.then(() => {
260+
setTimeout(() => {
261+
self.setLoading(false)
262+
}, 1000)
263+
})
250264
if (self.lgv.bpPerPx <= 3) {
251265
void (
252266
self.session as unknown as ApolloSessionModel

packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/layouts.ts

Lines changed: 83 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,39 @@ import { ApolloSessionModel } from '../../session'
1111
import { baseModelFactory } from './base'
1212
import { boxGlyph, geneGlyph, genericChildGlyph } from '../glyphs'
1313

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+
1447
export function layoutsModelFactory(
1548
pluginManager: PluginManager,
1649
configSchema: AnyConfigurationSchemaType,
@@ -19,48 +52,14 @@ export function layoutsModelFactory(
1952

2053
return BaseLinearApolloDisplay.named('LinearApolloDisplayLayouts')
2154
.props({
22-
featuresMinMaxLimit: 500_000,
55+
cleanupBoundary: 200_000,
2356
})
2457
.volatile(() => ({
2558
seenFeatures: observable.map<string, AnnotationFeature>(),
2659
}))
2760
.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)
6463
},
6564
getGlyph(feature: AnnotationFeature) {
6665
if (this.looksLikeGene(feature)) {
@@ -111,15 +110,11 @@ export function layoutsModelFactory(
111110
get featureLayouts() {
112111
const { assemblyManager } =
113112
self.session as unknown as AbstractSessionModel
114-
return self.lgv.displayedRegions.map((region, idx) => {
113+
return self.lgv.displayedRegions.map((region) => {
115114
const assembly = assemblyManager.get(region.assemblyName)
116-
const featureLayout = new Map<number, [number, AnnotationFeature][]>()
117-
const minMax = self.featuresMinMax[idx]
118-
if (!minMax) {
119-
return featureLayout
120-
}
121-
const [min, max] = minMax
122-
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][]>()
123118
const { end, refName, start } = region
124119
for (const [id, feature] of self.seenFeatures.entries()) {
125120
if (!isAlive(feature)) {
@@ -145,36 +140,24 @@ export function layoutsModelFactory(
145140
let startingRow = 0
146141
let placed = false
147142
while (!placed) {
148-
let rowsForFeature = rows.slice(
143+
let rowsForFeature = getRowsForFeature(
149144
startingRow,
150-
startingRow + rowCount,
145+
rowCount,
146+
filledRowLocations,
151147
)
152148
if (rowsForFeature.length < rowCount) {
153149
for (let i = 0; i < rowCount - rowsForFeature.length; i++) {
154-
const newRowNumber = rows.length
155-
rows[newRowNumber] = Array.from({ length: max - min })
150+
const newRowNumber = filledRowLocations.size
151+
filledRowLocations.set(newRowNumber, [])
156152
featureLayout.set(newRowNumber, [])
157153
}
158-
rowsForFeature = rows.slice(startingRow, startingRow + rowCount)
154+
rowsForFeature = getRowsForFeature(
155+
startingRow,
156+
rowCount,
157+
filledRowLocations,
158+
)
159159
}
160-
if (
161-
rowsForFeature
162-
.map((rowForFeature) => {
163-
// zero-length features are allowed in the spec
164-
const featureMax =
165-
feature.max - feature.min === 0
166-
? feature.min + 1
167-
: feature.max
168-
let start = feature.min - min,
169-
end = featureMax - min
170-
if (feature.min - min < 0) {
171-
start = 0
172-
end = featureMax - feature.min
173-
}
174-
return rowForFeature.slice(start, end).some(Boolean)
175-
})
176-
.some(Boolean)
177-
) {
160+
if (!canPlaceFeatureInRows(rowsForFeature, feature)) {
178161
startingRow += 1
179162
continue
180163
}
@@ -183,16 +166,9 @@ export function layoutsModelFactory(
183166
rowNum < startingRow + rowCount;
184167
rowNum++
185168
) {
186-
const row = rows[rowNum]
187-
let start = feature.min - min,
188-
end = feature.max - min
189-
if (feature.min - min < 0) {
190-
start = 0
191-
end = feature.max - feature.min
192-
}
193-
row.fill(true, start, end)
169+
filledRowLocations.get(rowNum)?.push([feature.min, feature.max])
194170
const layoutRow = featureLayout.get(rowNum)
195-
layoutRow?.push([rowNum - startingRow, feature])
171+
layoutRow?.push([rowNum - startingRow, feature._id])
196172
}
197173
placed = true
198174
}
@@ -206,12 +182,17 @@ export function layoutsModelFactory(
206182
self.session.apolloDataStore.ontologyManager
207183
for (const [idx, layout] of featureLayouts.entries()) {
208184
for (const [layoutRowNum, layoutRow] of layout) {
209-
for (const [featureRowNum, layoutFeature] of layoutRow) {
185+
for (const [featureRowNum, layoutFeatureId] of layoutRow) {
210186
if (featureRowNum !== 0) {
211187
// Same top-level feature in all feature rows, so only need to
212188
// check the first one
213189
continue
214190
}
191+
const layoutFeature =
192+
self.getAnnotationFeatureById(layoutFeatureId)
193+
if (!layoutFeature) {
194+
continue
195+
}
215196
if (feature._id === layoutFeature._id) {
216197
return {
217198
layoutIndex: idx,
@@ -257,6 +238,30 @@ export function layoutsModelFactory(
257238
if (!self.lgv.initialized || self.regionCannotBeRendered()) {
258239
return
259240
}
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
260265
for (const region of self.regions) {
261266
const assembly = (
262267
self.session as unknown as ApolloSessionModel

packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/mouseEvents.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,18 @@ export function mouseEventsModelIntermediateFactory(
139139
if (!layoutRow) {
140140
return mousePosition
141141
}
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+
})
145146
if (!foundFeature) {
146147
return mousePosition
147148
}
148-
const [featureRow, topLevelFeature] = foundFeature
149+
const [featureRow, topLevelFeatureId] = foundFeature
150+
const topLevelFeature = self.getAnnotationFeatureById(topLevelFeatureId)
151+
if (!topLevelFeature) {
152+
return mousePosition
153+
}
149154
const glyph = self.getGlyph(topLevelFeature)
150155
const { featureTypeOntology } =
151156
self.session.apolloDataStore.ontologyManager

packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/rendering.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,8 +413,9 @@ export function renderingModelFactory(
413413
for (const [idx, featureLayout] of featureLayouts.entries()) {
414414
const displayedRegion = displayedRegions[idx]
415415
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) {
418419
continue
419420
}
420421
if (

0 commit comments

Comments
 (0)