From 46e51739b7cae62b526a3cbaee0b996252de7d2f Mon Sep 17 00:00:00 2001 From: Colin Diesh Date: Fri, 2 Jul 2021 11:12:44 -0400 Subject: [PATCH] Make layout code not use observable map again to fix slowness (#2097) * Drive mouseovers using layout data structure * Intermediate * Fixup breakpoint split view * Fix unit tests * Fix up some lint and tsc * Standardize getById to return recttuple for both layouts * Remove unused breakpoint split viewer from linear-comparative-view --- packages/core/package.json | 1 + .../core/util/layouts/GranularRectLayout.ts | 41 +-- .../core/util/layouts/PrecomputedLayout.ts | 30 +++ .../LinearAlignmentsDisplay/models/model.tsx | 4 +- .../components/PileupRendering.tsx | 3 +- .../src/BreakpointSplitView.test.tsx | 24 +- .../breakpoint-split-view/src/model.test.tsx | 23 +- plugins/breakpoint-split-view/src/model.ts | 5 +- .../BreakpointSplitRenderer.ts | 243 ------------------ ...a-simple-synteny-from-fake-data-1-snap.png | Bin 6768 -> 0 bytes .../components/BreakpointSplitRendering.tsx | 90 ------- .../BreakpointSplitRenderer/configSchema.js | 20 -- .../src/BreakpointSplitRenderer/index.js | 3 - .../src/BreakpointSplitView/index.ts | 132 ---------- .../src/BreakpointSplitView/model.ts | 14 - plugins/linear-comparative-view/src/util.ts | 1 - plugins/linear-genome-view/package.json | 1 - .../models/BaseLinearDisplayModel.tsx | 57 +--- .../components/SvgFeatureRendering.js | 5 +- .../BreakpointSplitView.test.js.snap | 4 +- test_data/volvox/config_main_thread.json | 55 ++++ 21 files changed, 154 insertions(+), 602 deletions(-) delete mode 100644 plugins/linear-comparative-view/src/BreakpointSplitRenderer/BreakpointSplitRenderer.ts delete mode 100644 plugins/linear-comparative-view/src/BreakpointSplitRenderer/__image_snapshots__/linear-synteny-renderer-test-ts-test-rendering-a-simple-synteny-from-fake-data-1-snap.png delete mode 100644 plugins/linear-comparative-view/src/BreakpointSplitRenderer/components/BreakpointSplitRendering.tsx delete mode 100644 plugins/linear-comparative-view/src/BreakpointSplitRenderer/configSchema.js delete mode 100644 plugins/linear-comparative-view/src/BreakpointSplitRenderer/index.js delete mode 100644 plugins/linear-comparative-view/src/BreakpointSplitView/index.ts delete mode 100644 plugins/linear-comparative-view/src/BreakpointSplitView/model.ts diff --git a/packages/core/package.json b/packages/core/package.json index ea4fd5d13a..518fd37e0f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -53,6 +53,7 @@ "load-script2": "^2.0.5", "object.fromentries": "^2.0.0", "pako": "^1.0.10", + "rbush": "^3.0.1", "react-error-boundary": "^3.0.0", "react-intersection-observer": "^8.31.0", "react-measure": "^2.3.0", diff --git a/packages/core/util/layouts/GranularRectLayout.ts b/packages/core/util/layouts/GranularRectLayout.ts index f59b5a299e..4fa7ca5eaa 100644 --- a/packages/core/util/layouts/GranularRectLayout.ts +++ b/packages/core/util/layouts/GranularRectLayout.ts @@ -1,4 +1,3 @@ -import { ObservableMap } from 'mobx' import { objectFromEntries } from '../index' import { RectTuple, @@ -32,13 +31,13 @@ interface RowState { min: number max: number offset: number - bits: (Record | boolean | undefined)[] + bits: (Record | string | undefined)[] } // a single row in the layout class LayoutRow { private padding: number - private allFilled?: Record | boolean + private allFilled?: Record | string private widthLimit: number @@ -61,11 +60,11 @@ class LayoutRow { // console.log(`r${this.rowNumber}: ${msg}`) // } - setAllFilled(data: Record | boolean): void { + setAllFilled(data: Record | string): void { this.allFilled = data } - getItemAt(x: number): Record | boolean | undefined { + getItemAt(x: number): Record | string | undefined { if (this.allFilled) { return this.allFilled } @@ -129,7 +128,7 @@ class LayoutRow { // this.log(`initialize ${this.rowState.min} - ${this.rowState.max} (${this.rowState.bits.length})`) } - addRect(rect: Rectangle, data: Record | boolean): void { + addRect(rect: Rectangle, data: Record | string): void { const left = rect.l const right = rect.r + this.padding // only padding on the right if (!this.rowState) { @@ -319,7 +318,7 @@ export default class GranularRectLayout implements BaseLayout { private bitmap: LayoutRow[] - private rectangles: ObservableMap> + private rectangles: Map> public maxHeightReached: boolean @@ -329,6 +328,12 @@ export default class GranularRectLayout implements BaseLayout { private pTotalHeight: number + /* + * + * pitchX - layout grid pitch in the X direction + * pitchY - layout grid pitch in the Y direction + * maxHeight - maximum layout height, default Infinity (no max) + */ constructor({ pitchX = 10, pitchY = 10, @@ -336,11 +341,8 @@ export default class GranularRectLayout implements BaseLayout { hardRowLimit = 3000, displayMode = 'normal', }: { - /** layout grid pitch in the X direction */ pitchX?: number - /** layout grid pitch in the Y direction */ pitchY?: number - /** maximum layout height, default Infinity (no max) */ maxHeight?: number displayMode?: string hardRowLimit?: number @@ -358,7 +360,7 @@ export default class GranularRectLayout implements BaseLayout { } this.bitmap = [] - this.rectangles = new ObservableMap() + this.rectangles = new Map() this.maxHeight = Math.ceil(maxHeight / this.pitchY) this.pTotalHeight = 0 // total height, in units of bitmap squares (px/pitchY) } @@ -375,14 +377,14 @@ export default class GranularRectLayout implements BaseLayout { data?: Record, ): number | null { // if we have already laid it out, return its layout - // console.log(`${this.id} add ${id}`) const storedRec = this.rectangles.get(id) if (storedRec) { if (storedRec.top === null) { return null } - // add it to the bitmap again, since that bitmap range may have been discarded + // add it to the bitmap again, since that bitmap range may have been + // discarded this.addRectToBitmap(storedRec) return storedRec.top * this.pitchY } @@ -391,13 +393,11 @@ export default class GranularRectLayout implements BaseLayout { const pRight = Math.floor(right / this.pitchX) const pHeight = Math.ceil(height / this.pitchY) - // const midX = Math.floor((pLeft + pRight) / 2) const rectangle: Rectangle = { id, l: pLeft, r: pRight, top: null, - // mX: midX, h: pHeight, originalHeight: height, data, @@ -423,7 +423,6 @@ export default class GranularRectLayout implements BaseLayout { this.addRectToBitmap(rectangle) this.rectangles.set(id, rectangle) this.pTotalHeight = Math.max(this.pTotalHeight || 0, top + pHeight) - // console.log(`G2 ${data.get('name')} ${top}`) return top * this.pitchY } @@ -467,7 +466,7 @@ export default class GranularRectLayout implements BaseLayout { return } - const data = rect.data || true + const data = rect.data || rect.id const { bitmap } = this const yEnd = rect.top + rect.h if (rect.r - rect.l > maxFeaturePitchWidth) { @@ -508,7 +507,7 @@ export default class GranularRectLayout implements BaseLayout { return this.rectangles.has(id) } - getByCoord(x: number, y: number): Record | boolean | undefined { + getByCoord(x: number, y: number): Record | string | undefined { const pY = Math.floor(y / this.pitchY) const row = this.bitmap[pY] if (!row) { @@ -518,11 +517,13 @@ export default class GranularRectLayout implements BaseLayout { return row.getItemAt(pX) } - getByID(id: string): (Record | boolean) | undefined { + getByID(id: string): RectTuple | undefined { const r = this.rectangles.get(id) if (r) { - return r.data || true + const t = (r.top as number) * this.pitchX + return [r.l * this.pitchX, t, r.r * this.pitchX, t + r.originalHeight] } + return undefined } diff --git a/packages/core/util/layouts/PrecomputedLayout.ts b/packages/core/util/layouts/PrecomputedLayout.ts index e9c0a0161f..9b87fa889f 100644 --- a/packages/core/util/layouts/PrecomputedLayout.ts +++ b/packages/core/util/layouts/PrecomputedLayout.ts @@ -5,6 +5,15 @@ import { BaseLayout, Rectangle, } from './BaseLayout' +import RBush from 'rbush' + +export interface Layout { + minX: number + minY: number + maxX: number + maxY: number + name: string +} export default class PrecomputedLayout implements BaseLayout { private rectangles: Map @@ -13,11 +22,23 @@ export default class PrecomputedLayout implements BaseLayout { public maxHeightReached: boolean + private rbush: RBush + constructor({ rectangles, totalHeight, maxHeightReached }: SerializedLayout) { this.rectangles = new Map(Object.entries(rectangles)) // rectangles is of the form "featureId": [leftPx, topPx, rightPx, bottomPx] this.totalHeight = totalHeight this.maxHeightReached = maxHeightReached + this.rbush = new RBush() + for (const [key, layout] of Object.entries(rectangles)) { + this.rbush.insert({ + minX: layout[0], + minY: layout[1], + maxX: layout[2], + maxY: layout[3], + name: key, + }) + } } addRect(id: string) { @@ -44,6 +65,15 @@ export default class PrecomputedLayout implements BaseLayout { throw new Error('Method not implemented.') } + getByCoord(x: number, y: number) { + const rect = { minX: x, minY: y, maxX: x + 1, maxY: y + 1 } + return this.rbush.collides(rect) ? this.rbush.search(rect)[0].name : [] + } + + getByID(id: string) { + return this.rectangles.get(id) + } + addRectToBitmap(_rect: Rectangle, _data: Record): void { throw new Error('Method not implemented.') } diff --git a/plugins/alignments/src/LinearAlignmentsDisplay/models/model.tsx b/plugins/alignments/src/LinearAlignmentsDisplay/models/model.tsx index 0314415366..72fd60d2e8 100644 --- a/plugins/alignments/src/LinearAlignmentsDisplay/models/model.tsx +++ b/plugins/alignments/src/LinearAlignmentsDisplay/models/model.tsx @@ -65,8 +65,8 @@ const stateModelFactory = ( } }, - get layoutFeatures() { - return self.PileupDisplay.layoutFeatures + getFeatureByID(id: string) { + return self.PileupDisplay.getFeatureByID(id) }, get features() { diff --git a/plugins/alignments/src/PileupRenderer/components/PileupRendering.tsx b/plugins/alignments/src/PileupRenderer/components/PileupRendering.tsx index 2b4c38deef..69058d7fb7 100644 --- a/plugins/alignments/src/PileupRenderer/components/PileupRendering.tsx +++ b/plugins/alignments/src/PileupRenderer/components/PileupRendering.tsx @@ -151,12 +151,11 @@ function PileupRendering(props: { const px = region.reversed ? width - offsetX : offsetX const clientBp = region.start + bpPerPx * px - const feats = displayModel.getFeatureOverlapping( + const featIdUnderMouse = displayModel.getFeatureOverlapping( blockKey, clientBp, offsetY, ) - const featIdUnderMouse = feats.length ? feats[0].name : undefined if (onMouseMove) { onMouseMove(event, featIdUnderMouse) diff --git a/plugins/breakpoint-split-view/src/BreakpointSplitView.test.tsx b/plugins/breakpoint-split-view/src/BreakpointSplitView.test.tsx index 298294ba28..8e24762070 100644 --- a/plugins/breakpoint-split-view/src/BreakpointSplitView.test.tsx +++ b/plugins/breakpoint-split-view/src/BreakpointSplitView.test.tsx @@ -23,13 +23,19 @@ const getView = () => { type: 'FakeTrack', configuration: types.frozen(), displays: types.array( - types.model('FakeDisplay', { - type: 'FakeDisplay', - displayId: 'FakeDisplay', - configuration: types.frozen(), - features: types.frozen(), - layoutFeatures: types.frozen(), - }), + types + .model('FakeDisplay', { + type: 'FakeDisplay', + displayId: 'FakeDisplay', + configuration: types.frozen(), + layoutFeatures: types.frozen(), + features: types.frozen(), + }) + .views(self => ({ + getFeatureByID(id: string) { + return self.layoutFeatures[id] + }, + })), ), }) .actions(self => ({ @@ -37,11 +43,9 @@ const getView = () => { self.displays[0].features = new Map( Object.entries(self.displays[0].features), ) - self.displays[0].layoutFeatures = new Map( - Object.entries(self.displays[0].layoutFeatures), - ) }, })) + stubManager.addViewType( () => new ViewType({ diff --git a/plugins/breakpoint-split-view/src/model.test.tsx b/plugins/breakpoint-split-view/src/model.test.tsx index 7982451361..1259af07cd 100644 --- a/plugins/breakpoint-split-view/src/model.test.tsx +++ b/plugins/breakpoint-split-view/src/model.test.tsx @@ -23,13 +23,19 @@ const getView = () => { type: 'FakeTrack', configuration: types.frozen(), displays: types.array( - types.model('FakeDisplay', { - type: 'FakeDisplay', - displayId: 'FakeDisplay', - configuration: types.frozen(), - features: types.frozen(), - layoutFeatures: types.frozen(), - }), + types + .model('FakeDisplay', { + type: 'FakeDisplay', + displayId: 'FakeDisplay', + configuration: types.frozen(), + features: types.frozen(), + layoutFeatures: types.frozen(), + }) + .views(self => ({ + getFeatureByID(id: string) { + return self.layoutFeatures[id] + }, + })), ), }) .actions(self => ({ @@ -37,9 +43,6 @@ const getView = () => { self.displays[0].features = new Map( Object.entries(self.displays[0].features), ) - self.displays[0].layoutFeatures = new Map( - Object.entries(self.displays[0].layoutFeatures), - ) }, })) stubManager.addViewType( diff --git a/plugins/breakpoint-split-view/src/model.ts b/plugins/breakpoint-split-view/src/model.ts index 4c99694365..d8cadab18e 100644 --- a/plugins/breakpoint-split-view/src/model.ts +++ b/plugins/breakpoint-split-view/src/model.ts @@ -229,8 +229,9 @@ export default function stateModelFactory(pluginManager: any) { // use reverse to search the second track first const tracks = this.getMatchedTracks(trackConfigId) - const calc = (track: any, feat: Feature) => - track.displays[0].layoutFeatures.get(feat.id()) + const calc = (track: any, feat: Feature) => { + return track.displays[0].getFeatureByID(feat.id()) + } return features.map(c => c.map(feature => { diff --git a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/BreakpointSplitRenderer.ts b/plugins/linear-comparative-view/src/BreakpointSplitRenderer/BreakpointSplitRenderer.ts deleted file mode 100644 index 4a8c240adc..0000000000 --- a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/BreakpointSplitRenderer.ts +++ /dev/null @@ -1,243 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import ComparativeServerSideRendererType, { - RenderArgsDeserialized as ComparativeRenderArgsDeserialized, -} from '@jbrowse/core/pluggableElementTypes/renderers/ComparativeServerSideRendererType' -import { Feature } from '@jbrowse/core/util/simpleFeature' -import { readConfObject } from '@jbrowse/core/configuration' -import { - createCanvas, - createImageBitmap, -} from '@jbrowse/core/util/offscreenCanvasPonyfill' -import { - overlayYPos, - interstitialYPos, - getPxFromCoordinate, - LayoutRecord, - ReducedLinearGenomeView, -} from '../util' - -const [LEFT, , RIGHT] = [0, 1, 2, 3] - -export interface RenderArgsDeserialized - extends ComparativeRenderArgsDeserialized { - height: number - width: number - middle: boolean - highResolutionScaling: number - linkedTrack: string - views: ReducedLinearGenomeView[] -} - -export interface RenderArgsDeserializedWithImageData - extends RenderArgsDeserialized { - imageData: any -} - -// faster than localeCompare by 5x or so -const strcmp = new Intl.Collator(undefined, { - numeric: true, - sensitivity: 'base', -}).compare - -function instantiateTrackLayoutFeatures(views: ReducedLinearGenomeView[]) { - views.forEach(view => { - view.tracks.forEach(track => { - if (track.layoutFeatures) { - // @ts-ignore - track.layoutFeatures = new Map(track.layoutFeatures) - } - }) - }) - return views - // return views.map(view => { - // console.log(view) - // // view.tracks = [] - // console.log(view.tracks) - // return stateModelFactory(pluginManager).create(view) - // }) -} - -function* generateLayoutMatches( - views: ReducedLinearGenomeView[], - trackId: string, - middle: boolean, -) { - const feats = views - .map((view, index) => { - const track = views[index].tracks.find(t => t.configuration === trackId) - return view.features.map(feature => { - const layout = - track && !middle - ? // prettier-ignore - // @ts-ignore - track.layoutFeatures.get(String(feature.id())) - : ([feature.get('start'), 0, feature.get('end'), 0] as LayoutRecord) - return { - feature, - level: index, - refName: feature.get('refName'), - layout, - } - }) - }) - .flat() - .sort((a, b) => strcmp(a.feature.get('name'), b.feature.get('name'))) - - let currEmit = [feats[0]] - let currFeat: { feature: Feature; level: number } = feats[0] - - for (let i = 1; i < feats.length; i++) { - if (currFeat.feature.get('name') !== feats[i].feature.get('name')) { - yield currEmit - currFeat = feats[i] - currEmit = [feats[i]] - } else { - currEmit.push(feats[i]) - } - } -} - -function drawArrow( - context: CanvasRenderingContext2D, - fromx: number, - fromy: number, - tox: number, - toy: number, -) { - const headlen = 6 // length of head in pixels - const dx = tox - fromx - const dy = toy - fromy - const angle = Math.atan2(dy, dx) - context.beginPath() - context.moveTo(tox, toy) - context.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 8), - toy - headlen * Math.sin(angle - Math.PI / 6), - ) - context.lineTo( - tox - headlen * Math.cos(angle + Math.PI / 8), - toy - headlen * Math.sin(angle + Math.PI / 8), - ) - context.closePath() - context.fill() -} - -export default class BreakpointSplitRenderer extends ComparativeServerSideRendererType { - async makeImageData(props: RenderArgsDeserialized) { - const { - highResolutionScaling: scale = 1, - width, - height, - middle, - linkedTrack, - config, - } = props - - const trackId = linkedTrack - const canvas = createCanvas(Math.ceil(width * scale), height * scale) - const ctx = canvas.getContext('2d') - ctx.scale(scale, scale) - ctx.strokeStyle = readConfObject(config, 'color') - ctx.fillStyle = readConfObject(config, 'color') - const drawMode = readConfObject(config, 'drawMode') - const views = instantiateTrackLayoutFeatures(props.views) - - for (const chunk of generateLayoutMatches(views, trackId, middle)) { - // we follow a path in the list of chunks, not from top to bottom, just in series - // following x1,y1 -> x2,y2 - chunk.sort((a, b) => a.feature.get('clipPos') - b.feature.get('clipPos')) - for (let i = 0; i < chunk.length - 1; i += 1) { - const { layout: c1, feature: f1, level: level1, refName: ref1 } = chunk[ - i - ] - const { layout: c2, feature: f2, level: level2, refName: ref2 } = chunk[ - i + 1 - ] - const v1 = views[level1] - const v2 = views[level2] - - if (!c1 || !c2) { - console.warn('received null layout for a overlay feature') - continue - } - - // possible TODO - // restore refName mapping for alternative refNames - - // disable rendering connections in a single level - // if (!showIntraviewLinks && level1 === level2) { - // continue - // } - - const x1 = getPxFromCoordinate( - v1, - ref1, - c1[f1.get('strand') === -1 ? LEFT : RIGHT], - ) - const x2 = getPxFromCoordinate( - v2, - ref2, - c2[f2.get('strand') === -1 ? RIGHT : LEFT], - ) - - // const tracks = views.map(v => v.getTrack(trackConfigId)) - - const y1 = middle - ? interstitialYPos(level1 < level2, height) - : overlayYPos(trackId, level1, views, c1, level1 < level2) - const y2 = middle - ? interstitialYPos(level2 < level1, height) - : overlayYPos(trackId, level2, views, c2, level2 < level1) - - // possible todo: use totalCurveHeight to possibly make alternative squiggle if the S is too small - // - - if (drawMode === 'spline') { - ctx.beginPath() - ctx.moveTo(x1, y1) - ctx.bezierCurveTo( - x1 + 200 * f1.get('strand'), - y1, - x2 - 200 * f2.get('strand'), - y2, - x2, - y2, - ) - ctx.stroke() - drawArrow(ctx, x2 - 10 * f2.get('strand'), y2, x2, y2) - } else { - ctx.beginPath() - ctx.moveTo(x1 + 10 * f1.get('strand') * -1, y1) - ctx.lineTo(x1, y1) - ctx.lineTo(x2 - 10 * f2.get('strand') * -1, y2) - ctx.lineTo(x2, y2) - ctx.stroke() - // drawArrow( - // ctx, - // x2 - 10 * f2.get('strand') * flipMultipliers[level2], - // y2, - // x2, - // y2, - // ) - } - } - } - - const imageData = await createImageBitmap(canvas) - return { imageData } - } - - async render(renderProps: RenderArgsDeserialized) { - const { height, width } = renderProps - const { imageData } = await this.makeImageData(renderProps) - - const results = await super.render({ ...renderProps, imageData }) - - return { - ...results, - imageData, - height, - width, - } - } -} diff --git a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/__image_snapshots__/linear-synteny-renderer-test-ts-test-rendering-a-simple-synteny-from-fake-data-1-snap.png b/plugins/linear-comparative-view/src/BreakpointSplitRenderer/__image_snapshots__/linear-synteny-renderer-test-ts-test-rendering-a-simple-synteny-from-fake-data-1-snap.png deleted file mode 100644 index b2757f16d03185952201cf7a864fcb4ff49b1c9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6768 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 { - const ret = [0, 0] - if (displayModel && isStateTreeNode(displayModel)) { - // @ts-ignore - const { viewOffsets } = displayModel - const { views } = getParent(displayModel, 3) - for (let i = 0; i < views.length; i++) { - ret.push(views[i].offsetPx - viewOffsets[i]) - } - } - return ret - }, [displayModel]) - - const featureCanvas = useRef(null) - - useEffect(() => { - if (!imageData) { - return - } - const canvas = featureCanvas.current - if (!canvas) { - return - } - const context = canvas.getContext('2d') - if (!context) { - return - } - context.clearRect(0, 0, width, height) - context.resetTransform() - // see https://en.wikipedia.org/wiki/Transformation_matrix#/media/File:2D_affine_transformation_matrix.svg - context.transform(1, 0, -(voffs[1] - voffs[0]) / height, 1, -voffs[0], 0) - if (imageData.commands) { - imageData.commands.forEach( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (command: { style: string; type: string; args: any[] }) => { - if (command.type === 'strokeStyle') { - context.strokeStyle = command.style - } else if (command.type === 'fillStyle') { - context.fillStyle = command.style - } else if (command.type === 'font') { - context.font = command.style - } else { - // @ts-ignore - context[command.type](...command.args) - } - }, - ) - } else if (imageData instanceof ImageBitmapType) { - context.drawImage(imageData, 0, 0) - } else if (imageData.dataURL) { - const img = new Image() - img.onload = () => context.drawImage(img, 0, 0) - img.src = imageData.dataURL - } - }, [height, imageData, voffs, width]) - - return ( - - ) -} - -BreakpointSplitRendering.propTypes = { - displayModel: PropTypes.observableObject, -} - -export default observer(BreakpointSplitRendering) diff --git a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/configSchema.js b/plugins/linear-comparative-view/src/BreakpointSplitRenderer/configSchema.js deleted file mode 100644 index 586dab88a9..0000000000 --- a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/configSchema.js +++ /dev/null @@ -1,20 +0,0 @@ -import { ConfigurationSchema } from '@jbrowse/core/configuration' -import { types } from 'mobx-state-tree' - -export default ConfigurationSchema( - 'BreakpointSplitRenderer', - { - drawMode: { - type: 'stringEnum', - description: 'drawing mode', - model: types.enumeration('Rendering', ['spline', 'bracket']), - defaultValue: 'spline', - }, - color: { - type: 'color', - description: 'the color of each feature in a synteny', - defaultValue: 'black', - }, - }, - { explicitlyTyped: true }, -) diff --git a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/index.js b/plugins/linear-comparative-view/src/BreakpointSplitRenderer/index.js deleted file mode 100644 index dd088617e9..0000000000 --- a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { default as ReactComponent } from './components/BreakpointSplitRendering' -export { default as configSchema } from './configSchema' -export { default } from './BreakpointSplitRenderer' diff --git a/plugins/linear-comparative-view/src/BreakpointSplitView/index.ts b/plugins/linear-comparative-view/src/BreakpointSplitView/index.ts deleted file mode 100644 index e50ff18c32..0000000000 --- a/plugins/linear-comparative-view/src/BreakpointSplitView/index.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Feature } from '@jbrowse/core/util/simpleFeature' -import PluginManager from '@jbrowse/core/PluginManager' - -import { IAnyStateTreeNode } from 'mobx-state-tree' -import modelF from './model' -import componentF from '../LinearComparativeView/components/LinearComparativeView' - -export default ({ lib, load }: PluginManager) => { - const ViewType = lib['@jbrowse/core/pluggableElementTypes/ViewType'] - const { getSession } = lib['@jbrowse/core/util'] - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const ReactComponent = load(componentF) as any - const stateModel = load(modelF) - - class BreakpointSplitViewType extends ViewType { - snapshotFromBreakendFeature( - feature: Feature, - view: IAnyStateTreeNode & { - displayedRegions: [ - { assemblyName: string; start: number; end: number; refName: string }, - ] - }, - ) { - const breakendSpecification = (feature.get('ALT') || [])[0] - const startPos = feature.get('start') - let endPos - const bpPerPx = 10 - - // TODO: Figure this out for multiple assembly names - const { assemblyName } = view.displayedRegions[0] - const assembly = getSession(view).assemblyManager.get(assemblyName) - if (!assembly) { - throw new Error('assembly not yet loaded') - } - const { getCanonicalRefName } = assembly - const featureRefName = getCanonicalRefName(feature.get('refName')) - - const topRegion = view.displayedRegions.find( - (f: { refName: string }) => f.refName === String(featureRefName), - ) - - let mateRefName: string | undefined - let startMod = 0 - let endMod = 0 - - if (breakendSpecification) { - // a VCF breakend feature - if (breakendSpecification === '') { - const INFO = feature.get('INFO') || [] - endPos = INFO.END[0] - 1 - mateRefName = getCanonicalRefName(INFO.CHR2[0]) - } else if (breakendSpecification.MatePosition) { - const matePosition = breakendSpecification.MatePosition.split(':') - endPos = parseInt(matePosition[1], 10) - 1 - mateRefName = getCanonicalRefName(matePosition[0]) - if (breakendSpecification.Join === 'left') { - startMod = -1 - } - if (breakendSpecification.MateDirection === 'left') { - endMod = -1 - } - } - - // if (breakendSpecification.Join === 'left') { - // marker -1, else 0 - - // if (breakendSpecification.MateDirection === 'left') { - // marker -1, else 0 - } else if (feature.get('mate')) { - // a generic 'mate' feature - const mate = feature.get('mate') - mateRefName = getCanonicalRefName(mate.refName) - endPos = mate.start - } - - if (!mateRefName) { - console.warn( - `unable to resolve mate refName ${mateRefName} in reference genome`, - ) - return undefined - } - - const bottomRegion = view.displayedRegions.find( - f => f.refName === String(mateRefName), - ) - - if (!topRegion || !bottomRegion) { - console.warn( - `unable to find the refName for the top or bottom of the breakpoint view`, - ) - return undefined - } - - const topMarkedRegion = [{ ...topRegion }, { ...topRegion }] - const bottomMarkedRegion = [{ ...bottomRegion }, { ...bottomRegion }] - topMarkedRegion[0].end = startPos + startMod - topMarkedRegion[1].start = startPos + startMod - bottomMarkedRegion[0].end = endPos + endMod - bottomMarkedRegion[1].start = endPos + endMod - const snapshot = { - type: 'BreakpointSplitView', - views: [ - { - type: 'LinearGenomeView', - displayedRegions: topMarkedRegion, - hideHeader: true, - bpPerPx, - offsetPx: (topRegion.start + feature.get('start')) / bpPerPx, - }, - { - type: 'LinearGenomeView', - displayedRegions: bottomMarkedRegion, - hideHeader: true, - bpPerPx, - offsetPx: (bottomRegion.start + endPos) / bpPerPx, - }, - ], - displayName: `${ - feature.get('name') || feature.get('id') || 'breakend' - } split detail`, - } - return snapshot - } - } - - return new BreakpointSplitViewType({ - name: 'BreakpointSplitView', - stateModel, - ReactComponent, - }) -} diff --git a/plugins/linear-comparative-view/src/BreakpointSplitView/model.ts b/plugins/linear-comparative-view/src/BreakpointSplitView/model.ts deleted file mode 100644 index 0a7077fa59..0000000000 --- a/plugins/linear-comparative-view/src/BreakpointSplitView/model.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { types, Instance } from 'mobx-state-tree' -import baseModel from '../LinearComparativeView/model' - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export default function stateModelFactory(pluginManager: any) { - return types.compose( - baseModel(pluginManager), - types.model('BreakpointSplitView', { - type: types.literal('BreakpointSplitView'), - }), - ) -} -export type BreakpointSplitView = ReturnType -export type BreakpointSplitViewModel = Instance diff --git a/plugins/linear-comparative-view/src/util.ts b/plugins/linear-comparative-view/src/util.ts index dc330cc540..9b2b7e21e5 100644 --- a/plugins/linear-comparative-view/src/util.ts +++ b/plugins/linear-comparative-view/src/util.ts @@ -18,7 +18,6 @@ export interface ReducedLinearGenomeView { scrollTop: number height: number configuration: string - layoutFeatures: [string, LayoutRecord][] skip: number }[] } diff --git a/plugins/linear-genome-view/package.json b/plugins/linear-genome-view/package.json index fec6a21915..c6e5c8356a 100644 --- a/plugins/linear-genome-view/package.json +++ b/plugins/linear-genome-view/package.json @@ -43,7 +43,6 @@ "is-object": "^1.0.1", "json-stable-stringify": "^1.0.1", "normalize-wheel": "^1.0.1", - "rbush": "^3.0.1", "react-sizeme": "^2.6.7" }, "peerDependencies": { diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx index 62511e25e9..b510765559 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx @@ -18,7 +18,6 @@ import Typography from '@material-ui/core/Typography' import MenuOpenIcon from '@material-ui/icons/MenuOpen' import { autorun } from 'mobx' import { addDisposer, Instance, isAlive, types, getEnv } from 'mobx-state-tree' -import RBush from 'rbush' import React from 'react' import { Tooltip } from '../components/BaseLinearDisplay' import BlockState, { renderBlockData } from './serverSideRenderedBlock' @@ -117,8 +116,6 @@ export const BaseLinearDisplay = types }, })) .views(self => { - let stale = false // used to make rtree refresh, the mobx reactivity fails for some reason - let rbush: Record> = {} return { /** * a CompositeMap of `featureId -> feature obj` that @@ -131,7 +128,6 @@ export const BaseLinearDisplay = types featureMaps.push(block.features) } } - stale = true return new CompositeMap(featureMaps) }, @@ -155,53 +151,22 @@ export const BaseLinearDisplay = types layoutMaps.set(block.key, block.layout.getRectangles()) } } - stale = true return layoutMaps }, - /** - * a CompositeMap of `featureId -> feature obj` that - * just looks in all the block data for that feature - * - * when you are not using the rtree you can use this - * method because it still provides a stable reference - * of a featureId to a layout record (when using the - * rtree, you cross contaminate the coordinates) - */ - get layoutFeatures() { - const layoutMaps = [] - for (const block of self.blockState.values()) { - if (block && block.layout && block.layout.rectangles) { - layoutMaps.push(block.layout.getRectangles()) - } - } - stale = true // make rtree refresh - return new CompositeMap(layoutMaps) + + getFeatureOverlapping(blockKey: string, x: number, y: number) { + return self.blockState.get(blockKey)?.layout?.getByCoord(x, y) }, - get rtree() { - if (stale) { - rbush = {} as Record> - for (const [blockKey, layoutFeatures] of this.blockLayoutFeatures) { - rbush[blockKey] = new RBush() - const r = rbush[blockKey] - for (const [key, layout] of layoutFeatures) { - r.insert({ - minX: layout[0], - minY: layout[1], - maxX: layout[2], - maxY: layout[3], - name: key, - }) - } + getFeatureByID(id: string) { + let ret + self.blockState.forEach(block => { + const val = block?.layout?.getByID(id) + if (val) { + ret = val } - stale = false - } - return rbush - }, - getFeatureOverlapping(blockKey: string, x: number, y: number) { - const rect = { minX: x, minY: y, maxX: x + 1, maxY: y + 1 } - const rtree = this.rtree[blockKey] - return rtree && rtree.collides(rect) ? rtree.search(rect) : [] + }) + return ret }, } }) diff --git a/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js b/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js index 2ce313be06..4822f03a5a 100644 --- a/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js +++ b/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js @@ -273,14 +273,11 @@ function SvgFeatureRendering(props) { const px = region.reversed ? width - offsetX : offsetX const clientBp = region.start + bpPerPx * px - const feats = displayModel.getFeatureOverlapping( + const featureIdCurrentlyUnderMouse = displayModel.getFeatureOverlapping( blockKey, clientBp, offsetY, ) - const featureIdCurrentlyUnderMouse = feats.length - ? feats[0].name - : undefined if (onMouseMove) { onMouseMove(event, featureIdCurrentlyUnderMouse) diff --git a/products/jbrowse-web/src/tests/__snapshots__/BreakpointSplitView.test.js.snap b/products/jbrowse-web/src/tests/__snapshots__/BreakpointSplitView.test.js.snap index 5ddb28c417..641370571a 100644 --- a/products/jbrowse-web/src/tests/__snapshots__/BreakpointSplitView.test.js.snap +++ b/products/jbrowse-web/src/tests/__snapshots__/BreakpointSplitView.test.js.snap @@ -7,12 +7,12 @@ exports[`breakpoint split view open a split view 1`] = ` stroke="#333" > diff --git a/test_data/volvox/config_main_thread.json b/test_data/volvox/config_main_thread.json index 0c9723c742..7c111e3c94 100644 --- a/test_data/volvox/config_main_thread.json +++ b/test_data/volvox/config_main_thread.json @@ -36,5 +36,60 @@ } } } + ], + "tracks": [ + { + "type": "AlignmentsTrack", + "trackId": "volvox_bam_pileup", + "name": "volvox-sorted.bam (contigA LinearPileupDisplay)", + "category": ["Integration test"], + "assemblyNames": ["volvox"], + "adapter": { + "type": "BamAdapter", + "bamLocation": { + "uri": "volvox-sorted-altname.bam" + }, + "index": { + "location": { + "uri": "volvox-sorted-altname.bam.bai" + } + } + }, + "displays": [ + { + "type": "LinearPileupDisplay", + "displayId": "volvox_bam_pileup_pileup" + } + ] + }, + { + "type": "AlignmentsTrack", + "trackId": "volvox_alignments", + "name": "volvox-sorted.bam (ctgA, svg)", + "category": ["Integration test"], + "assemblyNames": ["volvox", "volvox2"], + "adapter": { + "type": "BamAdapter", + "bamLocation": { + "uri": "volvox-sorted.bam" + }, + "index": { + "location": { + "uri": "volvox-sorted.bam.bai" + } + } + }, + "displays": [ + { + "type": "LinearAlignmentsDisplay", + "displayId": "volvox_alignments_alignments", + "pileupDisplay": { + "type": "LinearPileupDisplay", + "displayId": "volvox_bam_altname_alignments_pileup", + "defaultRendering": "svg" + } + } + ] + } ] }