Skip to content

Commit

Permalink
Drive mouseovers using layout data structure
Browse files Browse the repository at this point in the history
  • Loading branch information
cmdcolin committed Jul 1, 2021
1 parent 002bacb commit 31d311e
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 64 deletions.
23 changes: 11 additions & 12 deletions packages/core/util/layouts/GranularRectLayout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ObservableMap } from 'mobx'
import { objectFromEntries } from '../index'
import {
RectTuple,
Expand Down Expand Up @@ -319,7 +318,7 @@ export default class GranularRectLayout<T> implements BaseLayout<T> {

private bitmap: LayoutRow<T>[]

private rectangles: ObservableMap<string, Rectangle<T>>
private rectangles: Map<string, Rectangle<T>>

public maxHeightReached: boolean

Expand All @@ -329,18 +328,21 @@ export default class GranularRectLayout<T> implements BaseLayout<T> {

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,
maxHeight = 10000,
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
Expand All @@ -358,7 +360,7 @@ export default class GranularRectLayout<T> implements BaseLayout<T> {
}

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)
}
Expand All @@ -375,14 +377,14 @@ export default class GranularRectLayout<T> implements BaseLayout<T> {
data?: Record<string, T>,
): 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
}
Expand All @@ -391,13 +393,11 @@ export default class GranularRectLayout<T> implements BaseLayout<T> {
const pRight = Math.floor(right / this.pitchX)
const pHeight = Math.ceil(height / this.pitchY)

// const midX = Math.floor((pLeft + pRight) / 2)
const rectangle: Rectangle<T> = {
id,
l: pLeft,
r: pRight,
top: null,
// mX: midX,
h: pHeight,
originalHeight: height,
data,
Expand All @@ -423,7 +423,6 @@ export default class GranularRectLayout<T> implements BaseLayout<T> {
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
}

Expand Down Expand Up @@ -467,7 +466,7 @@ export default class GranularRectLayout<T> implements BaseLayout<T> {
return
}

const data = rect.data || true
const data = rect.data || rect.id || true
const { bitmap } = this
const yEnd = rect.top + rect.h
if (rect.r - rect.l > maxFeaturePitchWidth) {
Expand Down
26 changes: 26 additions & 0 deletions packages/core/util/layouts/PrecomputedLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> implements BaseLayout<T> {
private rectangles: Map<string, RectTuple>
Expand All @@ -13,11 +22,23 @@ export default class PrecomputedLayout<T> implements BaseLayout<T> {

public maxHeightReached: boolean

private rbush: RBush<Layout>

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) {
Expand All @@ -44,6 +65,11 @@ export default class PrecomputedLayout<T> implements BaseLayout<T> {
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 : []
}

addRectToBitmap(_rect: Rectangle<T>, _data: Record<string, T>): void {
throw new Error('Method not implemented.')
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,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<string, RBush<Layout>> = {}
return {
/**
* a CompositeMap of `featureId -> feature obj` that
Expand All @@ -131,7 +129,6 @@ export const BaseLinearDisplay = types
featureMaps.push(block.features)
}
}
stale = true
return new CompositeMap<string, Feature>(featureMaps)
},

Expand All @@ -155,53 +152,11 @@ 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<string, LayoutRecord>(layoutMaps)
},

get rtree() {
if (stale) {
rbush = {} as Record<string, RBush<Layout>>
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,
})
}
}
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 self.blockState.get(blockKey)?.layout?.getByCoord(x, y)
},
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
55 changes: 55 additions & 0 deletions test_data/volvox/config_main_thread.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
]
}
]
}

0 comments on commit 31d311e

Please sign in to comment.