Scalebar and gridlines
Drag across the scalebar to zoom, hover to read the base.
View source — 207 lines
import { useRef, useState } from 'react'
import { usePalette } from '@jbrowse/core/ui/PaletteContext'
import { usePointerDrag } from '@jbrowse/core/util/usePointerDrag'
import {
EmbedProvider,
RegionSeams,
Scalebar,
TrackStack,
} from '@jbrowse/display-ui/embed'
import { useCreateViewState } from '@jbrowse/react-linear-genome-view2'
import { observer } from 'mobx-react'
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
const Gridlines = observer(function Gridlines({
view,
}: {
view: LinearGenomeViewModel
}) {
const palette = usePalette()
const path = (major: boolean) =>
view.gridlineTicks
.filter(tick => tick.major === major)
.map(tick => `M${tick.x + 0.5} 0V100000`)
.join('')
return (
<svg
aria-hidden
style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: view.staticBlocks.totalWidthPx,
transform: `translateX(${view.staticBlocksTranslateX}px)`,
pointerEvents: 'none',
}}
>
<path d={path(false)} style={{ stroke: palette.gridlineMinor }} />
<path d={path(true)} style={{ stroke: palette.gridlineMajor }} />
</svg>
)
})
function useDragToZoom(view: LinearGenomeViewModel) {
const [range, setRange] = useState<{ left: number; right: number }>()
const start = useRef({ anchor: 0, origin: 0 })
const span = (clientX: number) => {
const { anchor, origin } = start.current
const x = Math.min(Math.max(clientX - origin, 0), view.width)
return { left: Math.min(anchor, x), right: Math.max(anchor, x) }
}
const props = usePointerDrag({
onDragStart(event) {
const origin = event.currentTarget.getBoundingClientRect().left
start.current = { anchor: event.clientX - origin, origin }
},
onDrag(event) {
setRange(span(event.clientX))
},
onDragEnd(event) {
const { left, right } = span(event.clientX)
setRange(undefined)
if (right - left >= 4) {
view.moveTo(view.pxToBp(left), view.pxToBp(right))
}
},
})
return { range, props }
}
const PointerGuide = observer(function PointerGuide({
view,
x,
}: {
view: LinearGenomeViewModel
x: number
}) {
const palette = usePalette()
const { refName, coord, oob } = view.pxToBp(x)
return oob ? null : (
<div
data-testid="pointer-guide"
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
zIndex: 4,
pointerEvents: 'none',
transform: `translateX(${x}px)`,
}}
>
<div
style={{
position: 'absolute',
top: 0,
bottom: 0,
width: 1,
background: palette.text.primary,
}}
/>
<span
style={{
position: 'absolute',
top: 2,
left: Math.min(Math.max(-x, -60), view.width - x - 120),
width: 120,
textAlign: 'center',
fontSize: '0.7rem',
lineHeight: '16px',
whiteSpace: 'nowrap',
color: palette.text.primary,
background: palette.background.paper,
boxShadow: `0 0 0 1px ${palette.divider}`,
}}
>
{refName}:{coord.toLocaleString('en-US')}
</span>
</div>
)
})
const Demo = observer(function Demo({ view }: { view: LinearGenomeViewModel }) {
const palette = usePalette()
const { range, props } = useDragToZoom(view)
const [pointerX, setPointerX] = useState<number>()
return (
<div
onPointerMove={event => {
setPointerX(
event.clientX - event.currentTarget.getBoundingClientRect().left,
)
}}
onPointerLeave={() => {
setPointerX(undefined)
}}
>
<TrackStack view={view}>
<Gridlines view={view} />
<Scalebar
view={view}
data-testid="scalebar"
style={{ cursor: 'crosshair', touchAction: 'none' }}
{...props}
/>
<RegionSeams view={view} />
{range ? (
<div
data-testid="rubberband"
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: range.left,
width: range.right - range.left,
zIndex: 4,
pointerEvents: 'none',
background: `color-mix(in srgb, ${palette.primary.main} 20%, transparent)`,
}}
/>
) : pointerX === undefined ? null : (
<PointerGuide view={view} x={pointerX} />
)}
</TrackStack>
</div>
)
})
const ScalebarAndGridlines = observer(function ScalebarAndGridlines() {
const state = useCreateViewState({
assembly: {
name: 'hg38',
uri: 'https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz',
refNameAliases: {
uri: 'https://jbrowse.org/genomes/GRCh38/hg38_aliases.txt',
},
},
tracks: [
{
trackId: 'hg38_phylop',
name: 'phyloP 100-way conservation',
uri: 'https://jbrowse.org/demos/phylop/hg38.phyloP100way.brca1.bw',
displayDefaults: { height: 100, color: '#3a7ca5' },
},
{
trackId: 'hg38_genes',
name: 'RefSeq curated genes',
uri: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeqCurated.gff.gz',
index: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeqCurated.gff.gz.csi',
displayDefaults: { height: 120 },
},
],
view: {
loc: 'chr17:43,044,295..43,060,000 chr17:43,100,000..43,125,364',
tracks: ['hg38_phylop', 'hg38_genes'],
},
})
return state ? (
<EmbedProvider session={state.session}>
<Demo view={state.session.view} />
</EmbedProvider>
) : null
})
export default ScalebarAndGridlines