Skip to content

Commit

Permalink
Add mouseover display of read name to pileup
Browse files Browse the repository at this point in the history
  • Loading branch information
cmdcolin committed Jun 8, 2022
1 parent ba0975e commit 09ebca9
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 107 deletions.
78 changes: 63 additions & 15 deletions plugins/alignments/src/LinearPileupDisplay/model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { lazy } from 'react'
import { autorun, observable } from 'mobx'
import { cast, types, addDisposer, getEnv, Instance } from 'mobx-state-tree'
import copy from 'copy-to-clipboard'
import {
AnyConfigurationModel,
ConfigurationReference,
readConfObject,
getConf,
Expand All @@ -13,26 +17,25 @@ import {
Feature,
} from '@jbrowse/core/util'

import VisibilityIcon from '@material-ui/icons/Visibility'
import { ContentCopy as ContentCopyIcon } from '@jbrowse/core/ui/Icons'
import {
LinearGenomeViewModel,
BaseLinearDisplay,
} from '@jbrowse/plugin-linear-genome-view'
import { cast, types, addDisposer, getEnv, Instance } from 'mobx-state-tree'
import copy from 'copy-to-clipboard'

// icons
import VisibilityIcon from '@material-ui/icons/Visibility'
import { ContentCopy as ContentCopyIcon } from '@jbrowse/core/ui/Icons'
import MenuOpenIcon from '@material-ui/icons/MenuOpen'
import SortIcon from '@material-ui/icons/Sort'
import PaletteIcon from '@material-ui/icons/Palette'
import FilterListIcon from '@material-ui/icons/ClearAll'

import { autorun, observable } from 'mobx'
import { AnyConfigurationModel } from '@jbrowse/core/configuration/configurationSchema'
// locals
import { LinearPileupDisplayConfigModel } from './configSchema'
import LinearPileupDisplayBlurb from './components/LinearPileupDisplayBlurb'

import { getUniqueTagValues, getUniqueModificationValues } from '../shared'

// async
const ColorByTagDlg = lazy(() => import('./components/ColorByTag'))
const FilterByTagDlg = lazy(() => import('./components/FilterByTag'))
const SortByTagDlg = lazy(() => import('./components/SortByTag'))
Expand Down Expand Up @@ -94,6 +97,7 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
.volatile(() => ({
colorTagMap: observable.map<string, string>({}),
modificationTagMap: observable.map<string, string>({}),
featureUnderMouseVolatile: undefined as undefined | Feature,
ready: false,
}))
.actions(self => ({
Expand Down Expand Up @@ -128,7 +132,9 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
},

updateColorTagMap(uniqueTag: string[]) {
// pale color scheme https://cran.r-project.org/web/packages/khroma/vignettes/tol.html e.g. "tol_light"
// pale color scheme
// https://cran.r-project.org/web/packages/khroma/vignettes/tol.html
// e.g. "tol_light"
const colorPalette = [
'#BBCCEE',
'pink',
Expand All @@ -150,6 +156,9 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
}
})
},
setFeatureUnderMouse(feat?: Feature) {
self.featureUnderMouseVolatile = feat
},
}))
.actions(self => ({
afterAttach() {
Expand Down Expand Up @@ -337,6 +346,9 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
? self.mismatchAlpha
: readConfObject(this.rendererConfig, 'mismatchAlpha')
},
get featureUnderMouse() {
return self.featureUnderMouseVolatile
},
}))
.views(self => {
const {
Expand Down Expand Up @@ -395,6 +407,8 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
colorBy,
filterBy,
rpcDriverName,
currBpPerPx,
ready,
} = self

const superProps = superRenderProps()
Expand All @@ -403,8 +417,8 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
...superProps,
notReady:
superProps.notReady ||
!self.ready ||
(sortedBy && self.currBpPerPx !== view.bpPerPx),
!ready ||
(sortedBy && currBpPerPx !== view.bpPerPx),
rpcDriverName,
displayModel: self,
sortedBy,
Expand All @@ -414,7 +428,7 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
modificationTagMap: JSON.parse(JSON.stringify(modificationTagMap)),
showSoftClip: self.showSoftClipping,
config: self.rendererConfig,
async onFeatureClick(_: unknown, featureId: string | undefined) {
async onFeatureClick(_: unknown, featureId?: string) {
const session = getSession(self)
const { rpcManager } = session
try {
Expand Down Expand Up @@ -444,14 +458,48 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
session.notify(`${e}`)
}
},

async onMouseMove(_: unknown, featureId?: string) {
const session = getSession(self)
const { rpcManager } = session
self.setFeatureIdUnderMouse(featureId)
try {
const f = featureId || self.featureIdUnderMouse
if (!f) {
self.setFeatureUnderMouse(undefined)
} else {
const sessionId = getRpcSessionId(self)
const { feature } = (await rpcManager.call(
sessionId,
'CoreGetFeatureDetails',
{
featureId: f,
sessionId,
layoutId: getContainingView(self).id,
rendererType: 'PileupRenderer',
},
)) as { feature: unknown }

if (self.featureUnderMouse !== undefined) {
// @ts-ignore
self.setFeatureUnderMouse(new SimpleFeature(feature))
}
}
} catch (e) {
console.error(e)
session.notify(`${e}`)
}
},

onMouseLeave(_: unknown) {
self.setFeatureUnderMouse(undefined)
self.setFeatureIdUnderMouse(undefined)
},
onClick() {
self.clearFeatureSelection()
},
// similar to click but opens a menu with further options
async onFeatureContextMenu(
_: unknown,
featureId: string | undefined,
) {
async onFeatureContextMenu(_: unknown, featureId?: string) {
const session = getSession(self)
const { rpcManager } = session
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,109 +1,24 @@
import React, { useState, useRef, useMemo } from 'react'
import React, { useState, useRef } from 'react'
import { observer } from 'mobx-react'
import { Portal, alpha, useTheme, makeStyles } from '@material-ui/core'
import { useTheme, makeStyles } from '@material-ui/core'
import { getConf } from '@jbrowse/core/configuration'
import { Menu } from '@jbrowse/core/ui'
import { usePopper } from 'react-popper'

// locals
import Tooltip from './Tooltip'
import LinearBlocks from './LinearBlocks'
import { BaseLinearDisplayModel } from '../models/BaseLinearDisplayModel'

function round(value: number) {
return Math.round(value * 1e5) / 1e5
}

const useStyles = makeStyles(theme => ({
const useStyles = makeStyles({
display: {
position: 'relative',
whiteSpace: 'nowrap',
textAlign: 'left',
width: '100%',
minHeight: '100%',
},

// these styles come from
// https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Tooltip/Tooltip.js
tooltip: {
pointerEvents: 'none',
backgroundColor: alpha(theme.palette.grey[700], 0.9),
borderRadius: theme.shape.borderRadius,
color: theme.palette.common.white,
fontFamily: theme.typography.fontFamily,
padding: '4px 8px',
fontSize: theme.typography.pxToRem(12),
lineHeight: `${round(14 / 10)}em`,
maxWidth: 300,
wordWrap: 'break-word',
},
}))

const TooltipContents = React.forwardRef<
HTMLDivElement,
{ message: React.ReactNode | string }
>(({ message }: { message: React.ReactNode | string }, ref) => {
return <div ref={ref}>{message}</div>
})

const Tooltip = observer(
({
model,
clientMouseCoord,
}: {
model: BaseLinearDisplayModel
clientMouseCoord: Coord
}) => {
const classes = useStyles()
const { featureUnderMouse } = model
const [width, setWidth] = useState(0)
const [popperElt, setPopperElt] = useState<HTMLDivElement | null>(null)

// must be memoized a la https://github.com/popperjs/react-popper/issues/391
const virtElement = useMemo(
() => ({
getBoundingClientRect: () => {
const x = clientMouseCoord[0] + width / 2 + 20
const y = clientMouseCoord[1]
return {
top: y,
left: x,
bottom: y,
right: x,
width: 0,
height: 0,
x,
y,
toJSON() {},
}
},
}),
[clientMouseCoord, width],
)
const { styles, attributes } = usePopper(virtElement, popperElt)

const contents = featureUnderMouse
? getConf(model, 'mouseover', { feature: featureUnderMouse })
: undefined

return featureUnderMouse && contents ? (
<Portal>
<div
ref={setPopperElt}
className={classes.tooltip}
// zIndex needed to go over widget drawer
style={{ ...styles.popper, zIndex: 100000 }}
{...attributes.popper}
>
<TooltipContents
ref={elt => setWidth(elt?.getBoundingClientRect().width || 0)}
message={contents}
/>
</div>
</Portal>
) : null
},
)

type Coord = [number, number]
const BaseLinearDisplay = observer(
(props: { model: BaseLinearDisplayModel; children?: React.ReactNode }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { useState, useMemo } from 'react'
import { getConf } from '@jbrowse/core/configuration'
import { observer } from 'mobx-react'
import { Portal, alpha, makeStyles } from '@material-ui/core'
import { usePopper } from 'react-popper'

// locals
import { BaseLinearDisplayModel } from '../models/BaseLinearDisplayModel'

function round(value: number) {
return Math.round(value * 1e5) / 1e5
}
const useStyles = makeStyles(theme => ({
// these styles come from
// https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Tooltip/Tooltip.js
tooltip: {
pointerEvents: 'none',
backgroundColor: alpha(theme.palette.grey[700], 0.9),
borderRadius: theme.shape.borderRadius,
color: theme.palette.common.white,
fontFamily: theme.typography.fontFamily,
padding: '4px 8px',
fontSize: theme.typography.pxToRem(12),
lineHeight: `${round(14 / 10)}em`,
maxWidth: 300,
wordWrap: 'break-word',
},
}))

const TooltipContents = React.forwardRef<
HTMLDivElement,
{ message: React.ReactNode | string }
>(({ message }: { message: React.ReactNode | string }, ref) => {
return <div ref={ref}>{message}</div>
})

type Coord = [number, number]
const Tooltip = observer(
({
model,
clientMouseCoord,
}: {
model: BaseLinearDisplayModel
clientMouseCoord: Coord
}) => {
const classes = useStyles()
const { featureUnderMouse } = model
const [width, setWidth] = useState(0)
const [popperElt, setPopperElt] = useState<HTMLDivElement | null>(null)

// must be memoized a la https://github.com/popperjs/react-popper/issues/391
const virtElement = useMemo(
() => ({
getBoundingClientRect: () => {
const x = clientMouseCoord[0] + width / 2 + 20
const y = clientMouseCoord[1]
return {
top: y,
left: x,
bottom: y,
right: x,
width: 0,
height: 0,
x,
y,
toJSON() {},
}
},
}),
[clientMouseCoord, width],
)
const { styles, attributes } = usePopper(virtElement, popperElt)

const contents = featureUnderMouse
? getConf(model, 'mouseover', { feature: featureUnderMouse })
: undefined

return featureUnderMouse && contents ? (
<Portal>
<div
ref={setPopperElt}
className={classes.tooltip}
// zIndex needed to go over widget drawer
style={{ ...styles.popper, zIndex: 100000 }}
{...attributes.popper}
>
<TooltipContents
ref={elt => setWidth(elt?.getBoundingClientRect().width || 0)}
message={contents}
/>
</div>
</Portal>
) : null
},
)

export default Tooltip
Loading

0 comments on commit 09ebca9

Please sign in to comment.