diff --git a/plugins/alignments/src/LinearPileupDisplay/model.ts b/plugins/alignments/src/LinearPileupDisplay/model.ts index e7407d6589..d9ceaeeeb3 100644 --- a/plugins/alignments/src/LinearPileupDisplay/model.ts +++ b/plugins/alignments/src/LinearPileupDisplay/model.ts @@ -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, @@ -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')) @@ -94,6 +97,7 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) => .volatile(() => ({ colorTagMap: observable.map({}), modificationTagMap: observable.map({}), + featureUnderMouseVolatile: undefined as undefined | Feature, ready: false, })) .actions(self => ({ @@ -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', @@ -150,6 +156,9 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) => } }) }, + setFeatureUnderMouse(feat?: Feature) { + self.featureUnderMouseVolatile = feat + }, })) .actions(self => ({ afterAttach() { @@ -337,6 +346,9 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) => ? self.mismatchAlpha : readConfObject(this.rendererConfig, 'mismatchAlpha') }, + get featureUnderMouse() { + return self.featureUnderMouseVolatile + }, })) .views(self => { const { @@ -395,6 +407,8 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) => colorBy, filterBy, rpcDriverName, + currBpPerPx, + ready, } = self const superProps = superRenderProps() @@ -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, @@ -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 { @@ -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 { diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx index 96ee3b13fc..954cb36047 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx @@ -1,19 +1,15 @@ -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', @@ -21,89 +17,8 @@ const useStyles = makeStyles(theme => ({ 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
{message}
}) -const Tooltip = observer( - ({ - model, - clientMouseCoord, - }: { - model: BaseLinearDisplayModel - clientMouseCoord: Coord - }) => { - const classes = useStyles() - const { featureUnderMouse } = model - const [width, setWidth] = useState(0) - const [popperElt, setPopperElt] = useState(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 ? ( - -
- setWidth(elt?.getBoundingClientRect().width || 0)} - message={contents} - /> -
-
- ) : null - }, -) - type Coord = [number, number] const BaseLinearDisplay = observer( (props: { model: BaseLinearDisplayModel; children?: React.ReactNode }) => { diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/components/Tooltip.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/components/Tooltip.tsx new file mode 100644 index 0000000000..2ad06f1f28 --- /dev/null +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/components/Tooltip.tsx @@ -0,0 +1,98 @@ +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
{message}
+}) + +type Coord = [number, number] +const Tooltip = observer( + ({ + model, + clientMouseCoord, + }: { + model: BaseLinearDisplayModel + clientMouseCoord: Coord + }) => { + const classes = useStyles() + const { featureUnderMouse } = model + console.log({ featureUnderMouse }) + const [width, setWidth] = useState(0) + const [popperElt, setPopperElt] = useState(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 ? ( + +
+ setWidth(elt?.getBoundingClientRect().width || 0)} + message={contents} + /> +
+
+ ) : null + }, +) + +export default Tooltip diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx index bd807659de..ef0e632bef 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx @@ -551,7 +551,7 @@ export const BaseLinearDisplay = types self.currBpPerPx !== view.bpPerPx || !self.estimatedRegionStats, rpcDriverName: self.rpcDriverName, displayModel: self, - onFeatureClick(_: unknown, featureId: string | undefined) { + onFeatureClick(_: unknown, featureId?: string) { const f = featureId || self.featureIdUnderMouse if (!f) { self.clearFeatureSelection() @@ -566,7 +566,7 @@ export const BaseLinearDisplay = types self.clearFeatureSelection() }, // similar to click but opens a menu with further options - onFeatureContextMenu(_: unknown, featureId: string | undefined) { + onFeatureContextMenu(_: unknown, featureId?: string) { const f = featureId || self.featureIdUnderMouse if (!f) { self.clearFeatureSelection() @@ -576,7 +576,7 @@ export const BaseLinearDisplay = types } }, - onMouseMove(_: unknown, featureId: string | undefined) { + onMouseMove(_: unknown, featureId?: string) { self.setFeatureIdUnderMouse(featureId) },