Skip to content

Commit

Permalink
"Show all regions" for dotplot and synteny views and refactor synteny…
Browse files Browse the repository at this point in the history
… rendering RPC to optimize scrolling (#3440)

* Add show all regions to dotplot view

* Add showAllRegions button

* Draw multiple line with single stroke

* Subviews initialized

* Loop min/max/sum

* Misc code whitespace

* Only run bpToPx if needed

* Remove renderer concept from linear-comparative-view

* Use autorun/reaction to draw canvas contents instead of complicated useEffects

* Bump yarn.lock

* Remove renderer

* Update auto-generated docs

* Limit below which to draw line instead of box
  • Loading branch information
cmdcolin committed Jan 9, 2023
1 parent aa43be2 commit 0f9bf14
Show file tree
Hide file tree
Showing 59 changed files with 1,892 additions and 1,790 deletions.
17 changes: 10 additions & 7 deletions packages/core/data_adapters/BaseAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Observable, merge } from 'rxjs'
import { toArray } from 'rxjs/operators'
import { isStateTreeNode, getSnapshot } from 'mobx-state-tree'

// locals
import { ObservableCreate } from '../util/rxjs'
import { checkAbortSignal } from '../util'
import { checkAbortSignal, sum, max, min } from '../util'
import { Feature } from '../util/simpleFeature'
import {
readConfObject,
Expand Down Expand Up @@ -186,6 +188,7 @@ export abstract class BaseFeatureDataAdapter extends BaseAdapter {
* Currently this just calls getFeatureInRegion for each region. Adapters that
* are frequently called on multiple regions simultaneously may want to
* implement a more efficient custom version of this method.
*
* @param regions - Regions
* @param opts - Feature adapter options
* @returns Observable of Feature objects in the regions
Expand Down Expand Up @@ -224,12 +227,12 @@ export abstract class BaseFeatureDataAdapter extends BaseAdapter {
regions.map(region => this.getRegionStats(region, opts)),
)

const scoreMax = feats.map(a => a.scoreMax).reduce((a, b) => Math.max(a, b))
const scoreMin = feats.map(a => a.scoreMin).reduce((a, b) => Math.min(a, b))
const scoreSum = feats.reduce((a, b) => a + b.scoreSum, 0)
const scoreSumSquares = feats.reduce((a, b) => a + b.scoreSumSquares, 0)
const featureCount = feats.reduce((a, b) => a + b.featureCount, 0)
const basesCovered = feats.reduce((a, b) => a + b.basesCovered, 0)
const scoreMax = max(feats.map(a => a.scoreMax))
const scoreMin = min(feats.map(a => a.scoreMin))
const scoreSum = sum(feats.map(a => a.scoreSum))
const scoreSumSquares = sum(feats.map(a => a.scoreSumSquares))
const featureCount = sum(feats.map(a => a.featureCount))
const basesCovered = sum(feats.map(a => a.basesCovered))

return rectifyStats({
scoreMin,
Expand Down
97 changes: 71 additions & 26 deletions packages/core/util/Base1DUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getSnapshot } from 'mobx-state-tree'
import { ViewSnap } from './index'
import { Region, ViewSnap } from './index'

export interface BpOffset {
refName?: string
Expand Down Expand Up @@ -83,6 +83,10 @@ export function moveTo(
self.scrollTo(Math.round(bpToStart / self.bpPerPx))
}

function coord(r: Region, bp: number) {
return Math.floor(r.reversed ? r.end - bp : r.start + bp) + 1
}

// manual return type since getSnapshot hard to infer here
export function pxToBp(
self: ViewSnap,
Expand All @@ -109,16 +113,14 @@ export function pxToBp(
const blocks = staticBlocks.contentBlocks
const bp = (offsetPx + px) * bpPerPx
if (bp < 0) {
const region = displayedRegions[0]
const snap = getSnapshot(region)
const r = displayedRegions[0]
const snap = getSnapshot(r)
// @ts-ignore
return {
// xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
...(snap as Omit<typeof snap, symbol>),
oob: true,
coord: region.reversed
? Math.floor(region.end - bp) + 1
: Math.floor(region.start + bp) + 1,
coord: coord(r, bp),
offset: bp,
index: 0,
}
Expand All @@ -127,20 +129,18 @@ export function pxToBp(
const interRegionPaddingBp = interRegionPaddingWidth * bpPerPx
let currBlock = 0
for (let i = 0; i < displayedRegions.length; i++) {
const region = displayedRegions[i]
const len = region.end - region.start
const r = displayedRegions[i]
const len = r.end - r.start
const offset = bp - bpSoFar
if (len + bpSoFar > bp && bpSoFar <= bp) {
const snap = getSnapshot(region)
const snap = getSnapshot(r)
// @ts-ignore
return {
// xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
...(snap as Omit<typeof snap, symbol>),
oob: false,
offset,
coord: region.reversed
? Math.floor(region.end - offset) + 1
: Math.floor(region.start + offset) + 1,
coord: coord(r, offset),
index: i,
}
}
Expand All @@ -156,20 +156,18 @@ export function pxToBp(
}

if (bp >= bpSoFar && displayedRegions.length) {
const region = displayedRegions[displayedRegions.length - 1]
const len = region.end - region.start
const r = displayedRegions[displayedRegions.length - 1]
const len = r.end - r.start
const offset = bp - bpSoFar + len

const snap = getSnapshot(region)
const snap = getSnapshot(r)
// @ts-ignore
return {
// xref https://github.com/mobxjs/mobx-state-tree/issues/1524 for Omit
...(snap as Omit<typeof snap, symbol>),
oob: true,
offset,
coord: region.reversed
? Math.floor(region.end - offset) + 1
: Math.floor(region.start + offset) + 1,
coord: coord(r, offset),
index: displayedRegions.length - 1,
}
}
Expand Down Expand Up @@ -207,15 +205,11 @@ export function bpToPx({

let i = 0
for (; i < displayedRegions.length; i++) {
const region = displayedRegions[i]
const len = region.end - region.start
if (
refName === region.refName &&
coord >= region.start &&
coord <= region.end
) {
const r = displayedRegions[i]
const len = r.end - r.start
if (refName === r.refName && coord >= r.start && coord <= r.end) {
if (regionNumber ? regionNumber === i : true) {
bpSoFar += region.reversed ? region.end - coord : coord - region.start
bpSoFar += r.reversed ? r.end - coord : coord - r.start
break
}
}
Expand All @@ -239,3 +233,54 @@ export function bpToPx({

return undefined
}

export function bpToPxMap({
refName,
coord,
regionNumber,
self,
}: {
refName: string
coord: number
regionNumber?: number
self: ViewSnap
}) {
let bpSoFar = 0

const { interRegionPaddingWidth, bpPerPx, displayedRegions, staticBlocks } =
self
const blocks = staticBlocks.contentBlocks
const interRegionPaddingBp = interRegionPaddingWidth * bpPerPx
const map = {}
let currBlock = 0

let i = 0
for (; i < displayedRegions.length; i++) {
const r = displayedRegions[i]
const len = r.end - r.start
if (refName === r.refName && coord >= r.start && coord <= r.end) {
if (regionNumber !== undefined ? regionNumber === i : true) {
bpSoFar += r.reversed ? r.end - coord : coord - r.start
break
}
}

// add the interRegionPaddingWidth if the boundary is in the screen e.g. in
// a static block
if (blocks[currBlock]?.regionNumber === i) {
bpSoFar += len + interRegionPaddingBp
currBlock++
} else {
bpSoFar += len
}
}
const found = displayedRegions[i]
if (found) {
return {
index: i,
offsetPx: Math.round(bpSoFar / bpPerPx),
}
}

return map
}
38 changes: 33 additions & 5 deletions packages/core/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function useDebounce<T>(value: T, delay: number): T {
return debouncedValue
}

// https://stackoverflow.com/questions/56283920/how-to-debounce-a-callback-in-functional-component-using-hooks
// https://stackoverflow.com/questions/56283920/
export function useDebouncedCallback<A extends any[]>(
callback: (...args: A) => void,
wait = 400,
Expand Down Expand Up @@ -262,7 +262,7 @@ export function getContainingDisplay(node: IAnyStateTreeNode) {
* // ↳ 'chr1:1'
* ```
*/
export function assembleLocString(region: ParsedLocString): string {
export function assembleLocString(region: ParsedLocString) {
return assembleLocStringFast(region, toLocale)
}

Expand All @@ -272,7 +272,7 @@ export function assembleLocString(region: ParsedLocString): string {
export function assembleLocStringFast(
region: ParsedLocString,
cb = (n: number): string | number => n,
): string {
) {
const { assemblyName, refName, start, end, reversed } = region
const assemblyNameString = assemblyName ? `{${assemblyName}}` : ''
let startString
Expand Down Expand Up @@ -1181,8 +1181,8 @@ export function getStr(obj: unknown) {

// heuristic measurement for a column of a @mui/x-data-grid, pass in values from a column
export function measureGridWidth(elements: string[]) {
return Math.max(
...elements.map(element =>
return max(
elements.map(element =>
Math.min(Math.max(measureText(getStr(element), 14) + 50, 80), 1000),
),
)
Expand All @@ -1197,3 +1197,31 @@ export function localStorageGetItem(item: string) {
? localStorage.getItem(item)
: undefined
}

export function max(arr: number[]) {
let max = -Infinity
for (let i = 0; i < arr.length; i++) {
max = arr[i] > max ? arr[i] : max
}
return max
}

export function min(arr: number[]) {
let min = Infinity
for (let i = 0; i < arr.length; i++) {
min = arr[i] < min ? arr[i] : min
}
return min
}

export function sum(arr: number[]) {
let sum = 0
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
return sum
}

export function avg(arr: number[]) {
return sum(arr) / arr.length
}
75 changes: 75 additions & 0 deletions plugins/dotplot-view/src/DotplotView/1dview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { getParent, Instance } from 'mobx-state-tree'
import { observable } from 'mobx'
import Base1DView from '@jbrowse/core/util/Base1DViewModel'
import calculateDynamicBlocks from '@jbrowse/core/util/calculateDynamicBlocks'

/**
* #stateModel Dotplot1DView
* ref https://mobx-state-tree.js.org/concepts/volatiles on volatile state used here
*/
const Dotplot1DView = Base1DView.extend(self => {
const scaleFactor = observable.box(1)
return {
views: {
/**
* #getter
* this uses padding=false and elision=false
*/
get dynamicBlocks() {
return calculateDynamicBlocks(self, false, false)
},
/**
* #getter
*/

get scaleFactor() {
return scaleFactor.get()
},

/**
* #getter
*/
get maxBpPerPx() {
return self.totalBp / self.width
},
},
actions: {
/**
* #action
*/
setScaleFactor(n: number) {
scaleFactor.set(n)
},

/**
* #action
*/
center() {
const centerBp = self.totalBp / 2
const centerPx = centerBp / self.bpPerPx
self.scrollTo(Math.round(centerPx - self.width / 2))
},
},
}
})

const DotplotHView = Dotplot1DView.extend(self => ({
views: {
get width() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return getParent<any>(self).viewWidth
},
},
}))

const DotplotVView = Dotplot1DView.extend(self => ({
views: {
get width() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return getParent<any>(self).viewHeight
},
},
}))

export { DotplotVView, DotplotHView, Dotplot1DView }
export type Dotplot1DViewModel = Instance<typeof Dotplot1DView>
4 changes: 4 additions & 0 deletions plugins/dotplot-view/src/DotplotView/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ const DotplotControls = observer(({ model }: { model: DotplotViewModel }) => {
onClick: () => model.squareViewProportional(),
label: 'Rectanglular view - same total bp',
},
{
onClick: () => model.showAllRegions(),
label: 'Show all regions',
},
{
onClick: () => model.setDrawCigar(!model.drawCigar),
type: 'checkbox',
Expand Down
Loading

0 comments on commit 0f9bf14

Please sign in to comment.