Skip to content

Commit

Permalink
Global feature layout
Browse files Browse the repository at this point in the history
  • Loading branch information
cmdcolin committed Sep 25, 2024
1 parent 3187fe5 commit 26dfea7
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 106 deletions.
4 changes: 2 additions & 2 deletions packages/core/util/compositeMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ export default class CompositeMap<T, U> {

*[Symbol.iterator]() {
for (const key of this.keys()) {
yield [key, this.get(key)]
yield [key, this.get(key)] as const
}
}

*entries() {
for (const k of this.keys()) {
yield [k, this.get(k)]
yield [k, this.get(k)] as const
}
}
}
42 changes: 27 additions & 15 deletions plugins/canvas/src/CanvasFeatureRenderer/CanvasFeatureRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import BoxRendererType, {
RenderArgsDeserialized as BoxRenderArgsDeserialized,
} from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType'
import { BaseLayout } from '@jbrowse/core/util/layouts/BaseLayout'
import { iterMap, Feature } from '@jbrowse/core/util'
import { iterMap, Feature, notEmpty } from '@jbrowse/core/util'
import { renderToAbstractCanvas } from '@jbrowse/core/util/offscreenCanvasUtils'

// locals
Expand Down Expand Up @@ -42,8 +42,20 @@ export default class CanvasRenderer extends BoxRendererType {
const region = props.regions[0]!
const glyph =
feature.get('type') === 'gene' ? new GeneGlyph() : new BoxGlyph()
const fRect = glyph.layoutFeature({ region, ...props }, layout, feature)
return fRect ? { ...fRect, glyph } : null
const fRect = glyph.layoutFeature(
{
region,
...props,
},
layout,
feature,
)
return fRect
? {
...fRect,
glyph,
}
: null
}

drawRect(
Expand All @@ -61,9 +73,9 @@ export default class CanvasRenderer extends BoxRendererType {
layoutRecords: LaidOutFeatureRectWithGlyph[],
props: RenderArgsDeserializedWithFeaturesAndLayout,
) {
layoutRecords.forEach(fRect => {
for (const fRect of layoutRecords) {
this.drawRect(ctx, fRect, props)
})
}

if (props.exportSVG) {
postDraw({
Expand All @@ -87,7 +99,7 @@ export default class CanvasRenderer extends BoxRendererType {
featureMap.values(),
feature => this.layoutFeature(feature, layout, renderProps),
featureMap.size,
).filter((f): f is LaidOutFeatureRectWithGlyph => !!f)
).filter(notEmpty)

const width = (region.end - region.start) / bpPerPx
const height = Math.max(layout.getTotalHeight(), 1)
Expand Down Expand Up @@ -142,21 +154,21 @@ export function postDraw({
regions,
}: {
ctx: CanvasRenderingContext2D
regions: { start: number }[]
regions: {
start: number
}[]
offsetPx: number
layoutRecords: PostDrawFeatureRectWithGlyph[]
}) {
ctx.fillStyle = 'black'
ctx.font = '10px sans-serif'
layoutRecords
.filter(f => !!f)
.forEach(record => {
record.glyph.postDraw(ctx, {
record,
regions,
offsetPx,
})
layoutRecords.filter(notEmpty).forEach(record => {
record.glyph.postDraw(ctx, {
record,
regions,
offsetPx,
})
})
}

export {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import React, { useRef, useState, useEffect } from 'react'
import { Region } from '@jbrowse/core/util/types'
import { PrerenderedCanvas } from '@jbrowse/core/ui'
import { getContainingView } from '@jbrowse/core/util'
import { bpSpanPx } from '@jbrowse/core/util'
import { observer } from 'mobx-react'
import { isStateTreeNode } from 'mobx-state-tree'
import type {
BaseLinearDisplayModel,
LinearGenomeViewModel,
} from '@jbrowse/plugin-linear-genome-view'
import { postDraw } from '../CanvasFeatureRenderer'
import type { BaseLinearDisplayModel } from '@jbrowse/plugin-linear-genome-view'

// locals
import BoxGlyph from '../FeatureGlyphs/Box'
import GeneGlyph from '../FeatureGlyphs/Gene'
import { LaidOutFeatureRect } from '../FeatureGlyph'

// used so that user can click-away-from-feature below the laid out features
Expand All @@ -38,52 +29,17 @@ function CanvasRendering(props: {
height,
regions,
bpPerPx,
layoutRecords,
} = props

const { selectedFeatureId, featureIdUnderMouse, contextMenuFeature } =
displayModel || {}
const view = isStateTreeNode(displayModel)
? (getContainingView(displayModel) as LinearGenomeViewModel)
: undefined

const { dynamicBlocks, staticBlocks, offsetPx: viewOffsetPx = 0 } = view || {}
const { offsetPx: blockOffsetPx = 0 } = staticBlocks?.contentBlocks[0] || {}
const { start: viewStart } = dynamicBlocks?.contentBlocks[0] || {}
const offsetPx = viewOffsetPx - blockOffsetPx

const region = regions[0]!
const highlightOverlayCanvas = useRef<HTMLCanvasElement>(null)
const labelsCanvas = useRef<HTMLCanvasElement>(null)
const [mouseIsDown, setMouseIsDown] = useState(false)
const [movedDuringLastMouseDown, setMovedDuringLastMouseDown] =
useState(false)

useEffect(() => {
const canvas = labelsCanvas.current
if (!canvas) {
return
}
const ctx = canvas.getContext('2d')
if (!ctx) {
return
}

if (viewStart === undefined) {
return
}
ctx.clearRect(0, 0, canvas.width, canvas.height)
postDraw({
ctx,
layoutRecords: layoutRecords.map(rec => {
const glyph = rec.f.type === 'gene' ? new GeneGlyph() : new BoxGlyph()
return { ...rec, glyph }
}),
offsetPx,
regions: [{ start: viewStart }],
})
}, [layoutRecords, viewStart, offsetPx])

useEffect(() => {
const canvas = highlightOverlayCanvas.current
if (!canvas) {
Expand Down Expand Up @@ -264,18 +220,6 @@ function CanvasRendering(props: {
onFocus={() => {}}
onBlur={() => {}}
/>
<canvas
style={{
position: 'absolute',
left: 0,
top: 0,
zIndex: 1000,
pointerEvents: 'none',
}}
ref={labelsCanvas}
width={canvasWidth}
height={height + canvasPadding}
/>
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Menu } from '@jbrowse/core/ui'

import LinearBlocks from './LinearBlocks'
import { BaseLinearDisplayModel } from '../models/BaseLinearDisplayModel'
import { clamp, getContainingView } from '@jbrowse/core/util'
import { LinearGenomeViewModel } from '../../LinearGenomeView'

const useStyles = makeStyles()({
display: {
Expand All @@ -22,20 +24,50 @@ const useStyles = makeStyles()({

type Coord = [number, number]

const FloatingLabels = observer(function ({
model,
}: {
model: BaseLinearDisplayModel
}) {
const view = getContainingView(model) as LinearGenomeViewModel
const { bpPerPx, offsetPx } = view
return (
<div style={{ position: 'relative' }}>
{[...model.layoutFeatures.entries()].map(([key, val]) =>
val ? (
<div
key={key}
style={{
position: 'absolute',
fontSize: 10,
left: clamp(
0,
val[0] / bpPerPx - offsetPx,
val[2] / bpPerPx - offsetPx,
),
top: val[3] - 14,
}}
>
{key}
</div>
) : null,
)}
</div>
)
})

const BaseLinearDisplay = observer(function (props: {
model: BaseLinearDisplayModel
children?: React.ReactNode
}) {
const { classes } = useStyles()
const theme = useTheme()
const ref = useRef<HTMLDivElement>(null)
const [clientRect, setClientRect] = useState<DOMRect>()
const [offsetMouseCoord, setOffsetMouseCoord] = useState<Coord>([0, 0])
const [clientMouseCoord, setClientMouseCoord] = useState<Coord>([0, 0])
const [contextCoord, setContextCoord] = useState<Coord>()
const { model, children } = props
const { TooltipComponent, DisplayMessageComponent, height } = model
const items = model.contextMenuItems()
return (
<div
ref={ref}
Expand Down Expand Up @@ -67,6 +99,7 @@ const BaseLinearDisplay = observer(function (props: {
<LinearBlocks {...props} />
)}
{children}
<FloatingLabels model={model} />

<Suspense fallback={null}>
<TooltipComponent
Expand All @@ -78,38 +111,59 @@ const BaseLinearDisplay = observer(function (props: {
mouseCoord={offsetMouseCoord}
/>
</Suspense>

<Menu
open={Boolean(contextCoord) && items.length > 0}
onMenuItemClick={(_, callback) => {
callback()
setContextCoord(undefined)
}}
onClose={() => {
setContextCoord(undefined)
model.setContextMenuFeature(undefined)
}}
TransitionProps={{
onExit: () => {
setContextCoord(undefined)
model.setContextMenuFeature(undefined)
},
}}
anchorReference="anchorPosition"
anchorPosition={
contextCoord
? { top: contextCoord[1], left: contextCoord[0] }
: undefined
}
style={{
zIndex: theme.zIndex.tooltip,
}}
menuItems={items}
/>
{contextCoord ? (
<MenuPage
contextCoord={contextCoord}
model={model}
onClose={() => setContextCoord(undefined)}
/>
) : null}
</div>
)
})

function MenuPage({
onClose,
contextCoord,
model,
}: {
model: BaseLinearDisplayModel
contextCoord: Coord
onClose: () => void
}) {
const items = model.contextMenuItems()
const theme = useTheme()
return (
<Menu
open={items.length > 0}
onMenuItemClick={(_, callback) => {
callback()
onClose()
}}
onClose={() => {
onClose()
model.setContextMenuFeature(undefined)
}}
TransitionProps={{
onExit: () => {
onClose()
model.setContextMenuFeature(undefined)
},
}}
anchorReference="anchorPosition"
anchorPosition={
contextCoord
? { top: contextCoord[1], left: contextCoord[0] }
: undefined
}
style={{
zIndex: theme.zIndex.tooltip,
}}
menuItems={items}
/>
)
}

export default BaseLinearDisplay

export { default as Tooltip } from './Tooltip'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ function stateModelFactory() {
.views(self => ({
/**
* #getter
* a CompositeMap of `featureId -> feature obj` that
* just looks in all the block data for that feature
* a CompositeMap of `featureId -> feature obj` that just looks in all
* the block data for that feature
*/
get features() {
const featureMaps = []
Expand All @@ -168,6 +168,19 @@ function stateModelFactory() {
return feat ? this.features.get(feat) : undefined
},

/**
* #getter
*/
get layoutFeatures() {
const featureMaps = []
for (const block of self.blockState.values()) {
if (block.layout) {
featureMaps.push(block.layout.rectangles)
}
}
return new CompositeMap<string, LayoutRecord>(featureMaps)
},

/**
* #getter
*/
Expand Down

0 comments on commit 26dfea7

Please sign in to comment.